From b9c4576e99536e555503b01f70309718ed03f957 Mon Sep 17 00:00:00 2001 From: Sean Purcell Date: Wed, 4 Jul 2018 10:49:04 -0400 Subject: [PATCH 1/2] Create path.Stats endpoint and implement draw call stats For Vulkan, it creates a dependency graph for the synchronization primitives and uses that to determine which draw calls are complete. For other APIs it just assumes that all commands are blocking and executed in order. --- gapis/api/cmd_flags.go | 5 + gapis/api/sync/data.go | 35 ++++ gapis/api/templates/api.go.tmpl | 4 +- gapis/api/vulkan/api/draw_commands.api | 6 + gapis/api/vulkan/vulkan.go | 232 +++++++++++++++++++++++++ gapis/resolve/BUILD.bazel | 2 + gapis/resolve/resolve.go | 2 + gapis/resolve/stats.go | 169 ++++++++++++++++++ gapis/service/path/path.go | 6 + gapis/service/path/path.proto | 12 +- gapis/service/path/validate.go | 5 + gapis/service/service.go | 2 + gapis/service/service.proto | 11 +- 13 files changed, 486 insertions(+), 5 deletions(-) create mode 100644 gapis/resolve/stats.go diff --git a/gapis/api/cmd_flags.go b/gapis/api/cmd_flags.go index 6f8528f07c..a458cbd89b 100644 --- a/gapis/api/cmd_flags.go +++ b/gapis/api/cmd_flags.go @@ -26,6 +26,7 @@ const ( PushUserMarker PopUserMarker UserMarker + ExecutedDraw ) // IsDrawCall returns true if the command is a draw call. @@ -56,3 +57,7 @@ func (f CmdFlags) IsPopUserMarker() bool { return (f & PopUserMarker) != 0 } // marker. // The command may implement the Labeled interface to expose the marker name. func (f CmdFlags) IsUserMarker() bool { return (f & UserMarker) != 0 } + +// IsExecutedDraw returns true if the command is a draw call that gets executed +// as a subcommand. +func (f CmdFlags) IsExecutedDraw() bool { return (f & ExecutedDraw) != 0 } diff --git a/gapis/api/sync/data.go b/gapis/api/sync/data.go index 2d9bc06059..64cb398002 100644 --- a/gapis/api/sync/data.go +++ b/gapis/api/sync/data.go @@ -48,6 +48,29 @@ type SubcommandReference struct { IsCallerGroup bool } +// SyncNodeIdx is the identifier for a node in the sync dependency graph. +type SyncNodeIdx uint64 + +// SyncNode is the interface implemented by types that can be used as vertices +// in the sync dependency graph. +type SyncNode interface { + isSyncNode() +} + +// CmdNode is a node in the sync dependency graph that is a command. +type CmdNode struct { + Idx api.SubCmdIdx +} + +// AbstractNode is a node in the sync dependency graph that doesn't correspond +// to any point in the trace and is just used as a marker. +type AbstractNode struct{} + +var ( + _ = SyncNode(CmdNode{}) + _ = SyncNode(AbstractNode{}) +) + // Data contains a map of synchronization pairs. type Data struct { // CommandRanges contains commands that will be blocked from completion, @@ -65,6 +88,11 @@ type Data struct { // indexed by the immediate parent of the subcommands in the group. // e.g.: group: [73, 1, 4, 5~6] should be indexed by [73, 1, 4] SubCommandMarkerGroups *subCommandMarkerGroupTrie + // SyncDependencies contains the commands that must complete + // (according to their fences or semaphores) before they can be executed. + SyncDependencies map[SyncNodeIdx][]SyncNodeIdx + SyncNodes []SyncNode + CmdSyncNodes map[api.CmdID]SyncNodeIdx } type subCommandMarkerGroupTrie struct { @@ -95,6 +123,9 @@ func NewData() *Data { SubcommandGroups: map[api.CmdID][]api.SubCmdIdx{}, Hidden: api.CmdIDSet{}, SubCommandMarkerGroups: &subCommandMarkerGroupTrie{}, + SyncDependencies: map[SyncNodeIdx][]SyncNodeIdx{}, + SyncNodes: []SyncNode{}, + CmdSyncNodes: map[api.CmdID]SyncNodeIdx{}, } } @@ -132,3 +163,7 @@ func (e ExecutionRanges) SortedKeys() SynchronizationIndices { sort.Sort(v) return v } + +func (CmdNode) isSyncNode() {} + +func (AbstractNode) isSyncNode() {} diff --git a/gapis/api/templates/api.go.tmpl b/gapis/api/templates/api.go.tmpl index 065ff4cbf6..7d35188833 100644 --- a/gapis/api/templates/api.go.tmpl +++ b/gapis/api/templates/api.go.tmpl @@ -1299,8 +1299,8 @@ import ( } func (ϟc *{{$name}}) CmdFlags(ϟctx context.Context, ϟi ϟapi.CmdID, ϟg *ϟapi.GlobalState) ϟapi.CmdFlags { - {{$names := Strings "draw_call" "transform_feedback" "clear" "frame_start" "frame_end" "user_marker" "push_user_marker" "pop_user_marker"}} - {{$flags := Strings "DrawCall" "TransformFeedback" "Clear" "StartOfFrame" "EndOfFrame" "UserMarker" "PushUserMarker" "PopUserMarker"}} + {{$names := Strings "draw_call" "transform_feedback" "clear" "frame_start" "frame_end" "user_marker" "push_user_marker" "pop_user_marker" "executed_draw"}} + {{$flags := Strings "DrawCall" "TransformFeedback" "Clear" "StartOfFrame" "EndOfFrame" "UserMarker" "PushUserMarker" "PopUserMarker" "ExecutedDraw"}} var out ϟapi.CmdFlags {{range $i, $name := $names}} diff --git a/gapis/api/vulkan/api/draw_commands.api b/gapis/api/vulkan/api/draw_commands.api index b34fef09e6..11420bef4a 100644 --- a/gapis/api/vulkan/api/draw_commands.api +++ b/gapis/api/vulkan/api/draw_commands.api @@ -147,6 +147,7 @@ sub void dovkCmdDraw(ref!vkCmdDrawArgs draw) { @threadSafety("app") @indirect("VkCommandBuffer", "VkDevice") +@executed_draw cmd void vkCmdDraw( VkCommandBuffer commandBuffer, u32 vertexCount, @@ -198,6 +199,7 @@ sub void dovkCmdDrawIndexed(ref!vkCmdDrawIndexedArgs draw) { @threadSafety("app") @indirect("VkCommandBuffer", "VkDevice") +@executed_draw cmd void vkCmdDrawIndexed( VkCommandBuffer commandBuffer, u32 indexCount, @@ -243,6 +245,7 @@ sub void dovkCmdDrawIndirect(ref!vkCmdDrawIndirectArgs draw) { @threadSafety("app") @indirect("VkCommandBuffer", "VkDevice") +@executed_draw cmd void vkCmdDrawIndirect( VkCommandBuffer commandBuffer, VkBuffer buffer, @@ -285,6 +288,7 @@ sub void dovkCmdDrawIndexedIndirect(ref!vkCmdDrawIndexedIndirectArgs draw) { @threadSafety("app") @indirect("VkCommandBuffer", "VkDevice") +@executed_draw cmd void vkCmdDrawIndexedIndirect( VkCommandBuffer commandBuffer, VkBuffer buffer, @@ -314,6 +318,7 @@ sub void dovkCmdDispatch(ref!vkCmdDispatchArgs args) { @threadSafety("app") @indirect("VkCommandBuffer", "VkDevice") +@executed_draw cmd void vkCmdDispatch( VkCommandBuffer commandBuffer, u32 groupCountX, @@ -344,6 +349,7 @@ sub void dovkCmdDispatchIndirect(ref!vkCmdDispatchIndirectArgs dispatch) { @threadSafety("app") @indirect("VkCommandBuffer", "VkDevice") +@executed_draw cmd void vkCmdDispatchIndirect( VkCommandBuffer commandBuffer, VkBuffer buffer, diff --git a/gapis/api/vulkan/vulkan.go b/gapis/api/vulkan/vulkan.go index 194979d892..40fa5ff50b 100644 --- a/gapis/api/vulkan/vulkan.go +++ b/gapis/api/vulkan/vulkan.go @@ -17,6 +17,7 @@ package vulkan import ( "context" "fmt" + "sort" "github.com/google/gapid/core/log" "github.com/google/gapid/gapis/api" @@ -378,6 +379,237 @@ func (API) ResolveSynchronization(ctx context.Context, d *sync.Data, c *path.Cap } } + return dependencySync(ctx, d, c) +} + +func dependencySync(ctx context.Context, d *sync.Data, c *path.Capture) error { + st, err := capture.NewState(ctx) + if err != nil { + return err + } + cmds, err := resolve.Cmds(ctx, c) + if err != nil { + return err + } + + l := st.MemoryLayout + + addNode := func(pt sync.SyncNode) sync.SyncNodeIdx { + d.SyncNodes = append(d.SyncNodes, pt) + return sync.SyncNodeIdx(len(d.SyncNodes) - 1) + } + addDep := func(depender, dependee sync.SyncNodeIdx) { + v, _ := d.SyncDependencies[depender] + // append to nil is ok + d.SyncDependencies[depender] = append(v, dependee) + } + lastHostBarrier := addNode(sync.AbstractNode{}) + + getCmdNode := func(id api.CmdID) sync.SyncNodeIdx { + if pt, ok := d.CmdSyncNodes[id]; ok { + return pt + } + d.CmdSyncNodes[id] = addNode(sync.CmdNode{[]uint64{uint64(id)}}) + return d.CmdSyncNodes[id] + } + + semSignaler := map[VkSemaphore]sync.SyncNodeIdx{} + semDepend := func(idx sync.SyncNodeIdx, sem VkSemaphore) { + signaler, ok := semSignaler[sem] + if ok { + addDep(idx, signaler) + // Waiting on a semaphore clears the semaphore + delete(semSignaler, sem) + } + } + + fenceSignaler := map[VkFence]sync.SyncNodeIdx{} + fenceDepend := func(idx sync.SyncNodeIdx, fence VkFence) { + signaler, ok := fenceSignaler[fence] + if ok { + addDep(idx, signaler) + } + } + + queueSubmits := map[VkQueue][]sync.SyncNodeIdx{} + deviceQueues := map[VkDevice]map[VkQueue]struct{}{} + addSubmit := func(idx sync.SyncNodeIdx, queue VkQueue) { + qs, _ := queueSubmits[queue] + queueSubmits[queue] = append(qs, idx) + } + clearQueue := func(idx sync.SyncNodeIdx, queue VkQueue) { + qs, ok := queueSubmits[queue] + if ok { + delete(queueSubmits, queue) + val, _ := d.SyncDependencies[idx] + d.SyncDependencies[idx] = append(val, qs...) + } + } + + api.ForeachCmd(ctx, cmds, func(ctx context.Context, id api.CmdID, cmd api.Cmd) error { + // only track the commands that do anything + switch c := cmd.(type) { + case *VkQueueSubmit: + case *VkQueuePresentKHR: + case *VkCreateFence: + case *VkGetFenceStatus: + case *VkWaitForFences: + case *VkResetFences: + case *VkQueueWaitIdle: + case *VkDeviceWaitIdle: + case *VkGetDeviceQueue: + // Not part of the graph, just need to associate the queues with the device + c.Extras().Observations().ApplyReads(st.Memory.ApplicationPool()) + queue := c.PQueue().MustRead(ctx, c, st, nil) + if _, ok := deviceQueues[c.Device()]; !ok { + deviceQueues[c.Device()] = map[VkQueue]struct{}{} + } + deviceQueues[c.Device()][queue] = struct{}{} + return nil + default: + return nil + } + cmd.Extras().Observations().ApplyReads(st.Memory.ApplicationPool()) + cmdPt := getCmdNode(id) + addDep(cmdPt, lastHostBarrier) + switch c := cmd.(type) { + case *VkQueueSubmit: + submitCount := uint64(c.SubmitCount()) + submits := c.PSubmits().Slice(uint64(0), submitCount, l).MustRead(ctx, cmd, st, nil) + + submitSrcs := make([]sync.SyncNodeIdx, len(submits)) + submitDsts := make([]sync.SyncNodeIdx, len(submits)) + // For each submit, create an abstract node at each + // end. The start will depend on the semaphore + // signalers, and the end will be the sources for the + // next semaphores. + for i, s := range submits { + src := addNode(sync.AbstractNode{}) + dst := addNode(sync.AbstractNode{}) + + waitSems := s.PWaitSemaphores().Slice( + uint64(0), + uint64(s.WaitSemaphoreCount()), l). + MustRead(ctx, cmd, st, nil) + for _, s := range waitSems { + semDepend(src, s) + } + + signalSems := s.PSignalSemaphores().Slice( + uint64(0), + uint64(s.SignalSemaphoreCount()), l). + MustRead(ctx, cmd, st, nil) + for _, s := range signalSems { + semSignaler[s] = dst + } + + addDep(src, cmdPt) + + submitSrcs[i] = src + submitDsts[i] = dst + } + + type commandExecutor struct { + lastIdx api.SubCmdIdx + executor api.CmdID + } + // CommandRanges doesn't have a defined iteration + // order, so we need to sort it. + executors := make([]commandExecutor, 0, + len(d.CommandRanges[id].Ranges)) + for executor, lastIdx := range d.CommandRanges[id].Ranges { + executors = append(executors, commandExecutor{ + lastIdx: lastIdx, + executor: executor, + }) + } + sort.Slice(executors, func(i, j int) bool { + return executors[i].lastIdx.LessThan(executors[j].lastIdx) + }) + + excIdx := 0 + for _, subcmd := range d.SubcommandReferences[id] { + for excIdx < len(executors) && executors[excIdx].lastIdx.LessThan(subcmd.Index) { + excIdx++ + } + + sp := addNode(sync.CmdNode{ + append(api.SubCmdIdx{uint64(id)}, subcmd.Index...), + }) + sub := subcmd.Index[0] + // Make each subcommand depend on the source of + // the submit it came from, and be a dependency + // of the dst in the submit it came from. + addDep(sp, submitSrcs[sub]) + addDep(submitDsts[sub], sp) + addDep(sp, getCmdNode(executors[excIdx].executor)) + } + + donePt := addNode(sync.AbstractNode{}) + for _, dst := range submitDsts { + addDep(donePt, dst) + } + + fence := c.Fence() + if fence != VkFence(0) { + fenceSignaler[fence] = donePt + } + queue := c.Queue() + // If we do vkWaitQueueIdle then we depend on all previous submits. + addSubmit(donePt, queue) + case *VkQueuePresentKHR: + info := c.PPresentInfo().Slice(uint64(0), uint64(1), l).MustRead(ctx, cmd, st, nil)[0] + waitSems := info.PWaitSemaphores().Slice( + uint64(0), + uint64(info.WaitSemaphoreCount()), + l).MustRead(ctx, cmd, st, nil) + for _, s := range waitSems { + semDepend(cmdPt, s) + } + case *VkCreateFence: + signaledBit := VkFenceCreateFlags(VkFenceCreateFlagBits_VK_FENCE_CREATE_SIGNALED_BIT) + signaled := ((c.PCreateInfo(). + Slice(uint64(0), uint64(1), l). + MustRead(ctx, cmd, st, nil)[0]. + Flags()) & signaledBit) == signaledBit + if signaled { + fence := c.PFence().Slice(uint64(0), uint64(1), l).MustRead(ctx, cmd, st, nil)[0] + fenceSignaler[fence] = cmdPt + } + case *VkGetFenceStatus: + if c.Result() == VkResult_VK_SUCCESS { + fenceDepend(cmdPt, c.Fence()) + lastHostBarrier = cmdPt + } + case *VkWaitForFences: + fenceCount := uint64(c.FenceCount()) + if fenceCount == 1 || c.WaitAll() != VkBool32(0) { + // We can be sure all the fences were signaled + fences := c.PFences().Slice(uint64(0), fenceCount, l).MustRead(ctx, cmd, st, nil) + for _, f := range fences { + fenceDepend(cmdPt, f) + } + lastHostBarrier = cmdPt + } + case *VkResetFences: + fences := c.PFences().Slice(uint64(0), uint64(c.FenceCount()), l).MustRead(ctx, cmd, st, nil) + for _, f := range fences { + delete(fenceSignaler, f) + } + case *VkQueueWaitIdle: + clearQueue(cmdPt, c.Queue()) + lastHostBarrier = cmdPt + case *VkDeviceWaitIdle: + queues, ok := deviceQueues[c.Device()] + if ok { + for q := range queues { + clearQueue(cmdPt, q) + } + } + lastHostBarrier = cmdPt + } + return nil + }) return nil } diff --git a/gapis/resolve/BUILD.bazel b/gapis/resolve/BUILD.bazel index 55bdc0d5ec..a873595918 100644 --- a/gapis/resolve/BUILD.bazel +++ b/gapis/resolve/BUILD.bazel @@ -47,6 +47,7 @@ go_library( "set.go", "state.go", "state_tree.go", + "stats.go", "synchronization_data.go", "thumbnail.go", ], @@ -59,6 +60,7 @@ go_library( "//core/data/dictionary:go_default_library", "//core/data/endian:go_default_library", "//core/data/id:go_default_library", + "//core/data/pod:go_default_library", "//core/data/protoutil:go_default_library", "//core/event/task:go_default_library", "//core/fault:go_default_library", diff --git a/gapis/resolve/resolve.go b/gapis/resolve/resolve.go index 25d731da5a..4345eec542 100644 --- a/gapis/resolve/resolve.go +++ b/gapis/resolve/resolve.go @@ -320,6 +320,8 @@ func ResolveInternal(ctx context.Context, p path.Node) (interface{}, error) { return StateTreeNodeForPath(ctx, p) case *path.Thumbnail: return Thumbnail(ctx, p) + case *path.Stats: + return Stats(ctx, p) default: return nil, fmt.Errorf("Unknown path type %T", p) } diff --git a/gapis/resolve/stats.go b/gapis/resolve/stats.go new file mode 100644 index 0000000000..498f766bb0 --- /dev/null +++ b/gapis/resolve/stats.go @@ -0,0 +1,169 @@ +// Copyright (C) 2018 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resolve + +import ( + "context" + + "github.com/google/gapid/gapis/api" + "github.com/google/gapid/gapis/api/sync" + "github.com/google/gapid/gapis/capture" + "github.com/google/gapid/gapis/service" + "github.com/google/gapid/gapis/service/path" +) + +// Stats resolves and returns the stats list from the path p. +func Stats(ctx context.Context, p *path.Stats) (*service.Stats, error) { + stats := &service.Stats{} + if p.DrawCall { + err := drawCallStats(ctx, p.Capture, stats) + if err != nil { + return nil, err + } + } + return stats, nil +} + +func drawCallStats(ctx context.Context, capt *path.Capture, stats *service.Stats) error { + d, err := SyncData(ctx, capt) + if err != nil { + return err + } + cmds, err := Cmds(ctx, capt) + if err != nil { + return err + } + + st, err := capture.NewState(ctx) + if err != nil { + return err + } + flags := make([]api.CmdFlags, len(cmds)) + + // Get the present calls + events, err := Events(ctx, &path.Events{ + Capture: capt, + LastInFrame: true, + }) + if err != nil { + return err + } + + drawsPerFrame := make([]uint64, len(events.List)) + drawsSinceLastFrame := uint64(0) + + processed := map[sync.SyncNodeIdx]struct{}{} + + var process func(pt sync.SyncNodeIdx) error + process = func(pt sync.SyncNodeIdx) error { + if _, ok := processed[pt]; ok { + return nil + } + processed[pt] = struct{}{} + + ptObj := d.SyncNodes[pt] + if cmdIdx, ok := ptObj.(sync.CmdNode); ok { + idx := cmdIdx.Idx + cmd, err := Cmd(ctx, &path.Command{ + Capture: capt, + Indices: []uint64(idx), + }) + if err != nil { + return err + } + // If the command has subcommands, ignore it (vkQueueSubmit or similar) + if _, ok := d.SubcommandReferences[api.CmdID(idx[0])]; len(idx) > 1 || !ok { + var cmdflags api.CmdFlags + if len(idx) == 1 { + cmdflags = flags[idx[0]] + } else { + // NOTE: For subcommands its not clear + // what the "correct" state to present + // to CmdFlags is. Since Vulkan + // currently does not use the state, + // pass nil here instead of a + // potentially "incorrect" state. + cmdflags = cmd.CmdFlags(ctx, api.CmdID(idx[0]), nil) + } + if (len(idx) == 1 && cmdflags.IsDrawCall()) || + (len(idx) > 1 && cmdflags.IsExecutedDraw()) { + + drawsSinceLastFrame += 1 + } + } + } + + deps, ok := d.SyncDependencies[pt] + if ok { + for _, dep := range deps { + err := process(dep) + if err != nil { + return err + } + } + } + + return nil + } + + processCmd := func(idx uint64) error { + cmd := cmds[idx] + err := cmd.Mutate(ctx, api.CmdID(idx), st, nil) + if err != nil { + return err + } + flags[idx] = cmd.CmdFlags(ctx, api.CmdID(idx), st) + + // If the command wasn't included in the dependency graph, + // assume its a synchronous command (e.g. glDraw) + if _, ok := d.CmdSyncNodes[api.CmdID(idx)]; !ok { + if flags[idx].IsDrawCall() { + drawsSinceLastFrame += 1 + } + } + return nil + } + + cmdIdx := uint64(0) + for i, event := range events.List { + limitIdx := event.Command.Indices[0] + // Add any draws in the final unfinished frame to the last frame + if i == len(events.List)-1 { + limitIdx = uint64(len(cmds)) - 1 + } + for cmdIdx <= limitIdx { + err := processCmd(cmdIdx) + if err != nil { + return err + } + cmdIdx += 1 + } + id := api.CmdID(event.Command.Indices[0]) + cmd := cmds[id] + // If the frame boundary was on a synchronized api, process its dependencies + if _, ok := cmd.API().(sync.SynchronizedAPI); ok { + pt := d.CmdSyncNodes[id] + err := process(pt) + if err != nil { + return err + } + } + drawsPerFrame[i] = drawsSinceLastFrame + drawsSinceLastFrame = 0 + } + + stats.DrawCalls = drawsPerFrame + return nil +} diff --git a/gapis/service/path/path.go b/gapis/service/path/path.go index 2331c0e8ce..2d29b6660e 100644 --- a/gapis/service/path/path.go +++ b/gapis/service/path/path.go @@ -90,6 +90,7 @@ func (n *State) Path() *Any { return &Any{Path: &Any_State{n func (n *StateTree) Path() *Any { return &Any{Path: &Any_StateTree{n}} } func (n *StateTreeNode) Path() *Any { return &Any{Path: &Any_StateTreeNode{n}} } func (n *StateTreeNodeForPath) Path() *Any { return &Any{Path: &Any_StateTreeNodeForPath{n}} } +func (n *Stats) Path() *Any { return &Any{Path: &Any_Stats{n}} } func (n *Thumbnail) Path() *Any { return &Any{Path: &Any_Thumbnail{n}} } func (n API) Parent() Node { return nil } @@ -125,6 +126,7 @@ func (n State) Parent() Node { return n.After } func (n StateTree) Parent() Node { return n.State } func (n StateTreeNode) Parent() Node { return nil } func (n StateTreeNodeForPath) Parent() Node { return nil } +func (n Stats) Parent() Node { return n.Capture } func (n Thumbnail) Parent() Node { return oneOfNode(n.Object) } func (n *API) SetParent(p Node) {} @@ -154,6 +156,7 @@ func (n *State) SetParent(p Node) { n.After, _ = p.(*Command func (n *StateTree) SetParent(p Node) { n.State, _ = p.(*State) } func (n *StateTreeNode) SetParent(p Node) {} func (n *StateTreeNodeForPath) SetParent(p Node) {} +func (n *Stats) SetParent(p Node) { n.Capture, _ = p.(*Capture) } // Format implements fmt.Formatter to print the version. func (n ArrayIndex) Format(f fmt.State, c rune) { @@ -272,6 +275,9 @@ func (n StateTreeNodeForPath) Format(f fmt.State, c rune) { fmt.Fprintf(f, "state-tree-for<%v, %v>", n.Tree, n.Member) } +// Format implements fmt.Formatter to print the version. +func (n Stats) Format(f fmt.State, c rune) { fmt.Fprintf(f, "%v.stats", n.Parent()) } + // Format implements fmt.Formatter to print the version. func (n Thumbnail) Format(f fmt.State, c rune) { fmt.Fprintf(f, "%v.thumbnail", n.Parent()) } diff --git a/gapis/service/path/path.proto b/gapis/service/path/path.proto index c3153ac189..15d5b7c331 100644 --- a/gapis/service/path/path.proto +++ b/gapis/service/path/path.proto @@ -59,7 +59,8 @@ message Any { StateTree state_tree = 31; StateTreeNode state_tree_node = 32; StateTreeNodeForPath state_tree_node_for_path = 33; - Thumbnail thumbnail = 34; + Stats stats = 34; + Thumbnail thumbnail = 35; } } @@ -436,6 +437,15 @@ message StateTreeNodeForPath { Any member = 2; } +// Stats requests statistics for a given capture. Resolves to service.Stats. +message Stats { + // The capture to analyze + Capture capture = 1; + + // Whether to compute draw calls per frame statistics + bool draw_call = 2; +} + // Thumbnail is a path to a thumbnail image representing the object. message Thumbnail { // The desired maximum width of the thumbnail image. diff --git a/gapis/service/path/validate.go b/gapis/service/path/validate.go index 2a89580cd5..e0394f09b8 100644 --- a/gapis/service/path/validate.go +++ b/gapis/service/path/validate.go @@ -287,6 +287,11 @@ func (n *StateTreeNodeForPath) Validate() error { ) } +// Validate checks the path is valid. +func (n *Stats) Validate() error { + return checkNotNilAndValidate(n, n.Capture, "capture") +} + // Validate checks the path is valid. func (n *Thumbnail) Validate() error { return checkNotNilAndValidate(n, protoutil.OneOf(n.Object), "object") diff --git a/gapis/service/service.go b/gapis/service/service.go index 6e03214eda..9058b1ec16 100644 --- a/gapis/service/service.go +++ b/gapis/service/service.go @@ -229,6 +229,8 @@ func NewValue(v interface{}) *Value { return &Value{Val: &Value_StateTree{v}} case *StateTreeNode: return &Value{Val: &Value_StateTreeNode{v}} + case *Stats: + return &Value{Val: &Value_Stats{v}} case *api.Command: return &Value{Val: &Value_Command{v}} case *api.Mesh: diff --git a/gapis/service/service.proto b/gapis/service/service.proto index 7a5d556362..29525fc6d4 100644 --- a/gapis/service/service.proto +++ b/gapis/service/service.proto @@ -91,8 +91,9 @@ message Value { Resources resources = 13; StateTree state_tree = 14; StateTreeNode state_tree_node = 15; - Thread thread = 16; - Threads threads = 17; + Stats stats = 16; + Thread thread = 17; + Threads threads = 18; device.Instance device = 20; @@ -664,6 +665,12 @@ message ReportItem { repeated MsgRef tags = 4; } +// Stats stores the statistics for a capture +message Stats { + // The draw calls per frame, if requested in the path.Stats. + repeated uint64 draw_calls = 1; +} + // Thread represents a single thread in the capture. message Thread { string name = 1; From 8a5d2c19afcdf0e2ea481863bfda94f2edc202a8 Mon Sep 17 00:00:00 2001 From: Sean Purcell Date: Wed, 4 Jul 2018 10:53:19 -0400 Subject: [PATCH 2/2] Implement draw calls per frame for gapit stats --- cmd/gapit/stats.go | 58 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/cmd/gapit/stats.go b/cmd/gapit/stats.go index be99bfe27d..b8f814ef2c 100644 --- a/cmd/gapit/stats.go +++ b/cmd/gapit/stats.go @@ -19,8 +19,10 @@ import ( "flag" "fmt" "math" + "os" "path/filepath" "sort" + "text/tabwriter" "github.com/google/gapid/core/app" "github.com/google/gapid/core/log" @@ -70,7 +72,6 @@ func (verb *infoVerb) getEventsInRange(ctx context.Context, client service.Servi events, err := getEvents(ctx, client, &path.Events{ Capture: capture, AllCommands: true, - DrawCalls: true, FirstInFrame: true, LastInFrame: true, FramebufferObservations: true, @@ -114,6 +115,34 @@ func (verb *infoVerb) getEventsInRange(ctx context.Context, client service.Servi return events[begin:end], nil } +func (verb *infoVerb) drawCallStats(ctx context.Context, client client.Client, c *path.Capture) (int, sint.HistogramStats, error) { + boxedVal, err := client.Get(ctx, (&path.Stats{ + Capture: c, + DrawCall: true, + }).Path()) + if err != nil { + return 0, sint.HistogramStats{}, err + } + data := boxedVal.(*service.Stats).DrawCalls + + if verb.Frames.Start < len(data) { + data = data[verb.Frames.Start:] + } else { + data = []uint64{} + } + if verb.Frames.Count >= 0 && verb.Frames.Count < len(data) { + data = data[:verb.Frames.Count] + } + + hist := make(sint.Histogram, len(data)) + totalDraws := 0 + for i, dat := range data { + totalDraws += int(dat) + hist[i] = int(dat) + } + return totalDraws, hist.Stats(), nil +} + func (verb *infoVerb) Run(ctx context.Context, flags flag.FlagSet) error { client, capture, err := loadCapture(ctx, flags, verb.Gapis) if err != nil { @@ -141,14 +170,25 @@ func (verb *infoVerb) Run(ctx context.Context, flags flag.FlagSet) error { } } callStats := cmdsPerFrame.Stats() + totalDraws, drawStats, err := verb.drawCallStats(ctx, client, capture) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 4, 4, 0, ' ', 0) + fmt.Fprintf(w, "Commands: \t%v\n", counts[service.EventKind_AllCommands]) + fmt.Fprintf(w, "Frames: \t%v\n", counts[service.EventKind_FirstInFrame]) + fmt.Fprintf(w, "Draws: \t%v\n", totalDraws) + fmt.Fprintf(w, "FBO: \t%v\n", counts[service.EventKind_FramebufferObservation]) + + fmt.Fprintf(w, "Avg commands per frame: \t%.2f\n", callStats.Average) + fmt.Fprintf(w, "Stddev commands per frame: \t%.2f\n", callStats.Stddev) + fmt.Fprintf(w, "Median commands per frame: \t%v\n", callStats.Median) - fmt.Println("Commands: ", counts[service.EventKind_AllCommands]) - fmt.Println("Frames: ", counts[service.EventKind_FirstInFrame]) - fmt.Println("Draws: ", counts[service.EventKind_DrawCall]) - fmt.Println("FBO: ", counts[service.EventKind_FramebufferObservation]) - fmt.Printf("Avg commands per frame: %.2f\n", callStats.Average) - fmt.Printf("Stddev commands per frame: %.2f\n", callStats.Stddev) - fmt.Println("Median commands per frame: ", callStats.Median) + fmt.Fprintf(w, "Avg draw calls per frame: \t%.2f\n", drawStats.Average) + fmt.Fprintf(w, "Stddev draw calls per frame: \t%.2f\n", drawStats.Stddev) + fmt.Fprintf(w, "Median draw calls per frame: \t%v\n", drawStats.Median) + w.Flush() - return err + return nil }