| /** |
| * @module modules/commands-sk |
| |
| * @description A list view of draw commands for a single frame, and a tool for |
| * filtering them. contains the logic for processing the parsed json object from |
| * wasm, and extracting things like layer info and command counts which drive |
| * other modules. |
| * |
| * Contains play-sk as a submodule, which playes over the filtered list of |
| * commands. |
| * |
| * Data flows along this path in one direction depending on which end triggers a |
| * change. |
| * filter text box <=> this._includedSet <=> histogram-sk |
| * |
| * @evt histogram-update: An event containing the list of histogram entries. |
| * Emitted every time the histogram is recomputed. |
| * |
| * @evt move-command-position: When the play-sk module or user selects a different |
| * command, this event is emitted, and it's detail contains the command index in |
| * the unfiltered command list for this frame. |
| * |
| */ |
| import { define } from 'elements-sk/define'; |
| import { html, TemplateResult } from 'lit-html'; |
| import { ElementDocSk } from '../element-doc-sk/element-doc-sk'; |
| import { PlaySk, PlaySkMoveToEventDetail } from '../play-sk/play-sk'; |
| import { HistogramSkToggleEventDetail } from '../histogram-sk/histogram-sk' |
| import { DefaultMap } from '../default-map'; |
| |
| import 'elements-sk/icon/save-icon-sk'; |
| import 'elements-sk/icon/content-copy-icon-sk'; |
| import 'elements-sk/icon/image-icon-sk'; |
| |
| import { SkpJsonCommandList, SkpJsonCommand, SkpJsonAuditTrail, SkpJsonGpuOp } from '../debugger'; |
| |
| import '../play-sk'; |
| |
| export interface CommandsSkMovePositionEventDetail { |
| // the index of a command in the frame to which the wasm view should move. |
| position: number, |
| // true if we're currently paused |
| paused: boolean, |
| } |
| |
| export type CommandRange = [number, number]; |
| |
| // Represents one of the icons that can appear on a command |
| export interface PrefixItem { |
| icon: string, |
| color: string, |
| count: number, |
| }; |
| |
| /** A processed command object, created from a SkpJsonCommand */ |
| export interface Command { |
| // Index of the command in the unfiltered list. |
| index: number, |
| // the save/restore depth before this command is executed. |
| depth: number, |
| // the parsed json representation of the command. exact type depends on the command. |
| details: SkpJsonCommand, |
| name: string, |
| // if this command is one of an indenting pair, the command index range that the pair enclose |
| // (save, restore) |
| range?: CommandRange, |
| prefixes: PrefixItem[], |
| // Whether the command will be executed during playback |
| visible: boolean, |
| // index of any image referenced by this command |
| imageIndex?: number, |
| }; |
| |
| /** An entry of the command histogram |
| * obtained by totalling up occurances in the range filtered command list |
| */ |
| export interface HistogramEntry { |
| // name of a command (original CamelCase) |
| name: string, |
| // number of occurances in the current frame (or the whole file for a single-frame SKP) |
| countInFrame: number, |
| // number of occurances in the current range filter |
| countInRange: number, |
| } |
| |
| /** An event detail containing a new histogram |
| * or new filter set to be displayed by the histogram-sk module. |
| * The event may update one or both of the two fields. |
| */ |
| export interface CommandsSkHistogramEventDetail { |
| /** A newly computed histogram that needs to be displayed by histogram-sk */ |
| hist?: HistogramEntry[]; |
| /** whether the command is include by the filter */ |
| included?: Set<string>; |
| } |
| |
| // Information about layers collected by processCommands. |
| // TODO(nifong): This could be collected in the C++ and returned from |
| // getLayerSummaries and then commands-sk wouldn't have to be involved |
| // with layer things at all. |
| export interface LayerInfo { |
| // A Map from layer ids to command indices where they were drawn |
| // with a DrawImageRectLayer command. Includes only layer used this frame |
| uses: DefaultMap<number, number[]>; |
| // A map from layer ids to names that were provided in the render node annotations. |
| // This should be sufficient for it to always contain what we attempt to look up. |
| // Only valid for the duration of this frame. |
| names: Map<number, string>; |
| } |
| |
| // Jumpting to a command by it's unfiltered index can be done by emitting |
| // 'jump-command' with this event detail |
| export interface CommandsSkJumpEventDetail { |
| unfilteredIndex: number; |
| } |
| |
| // event issued when the user clicks 'Image' to jump to this image with this id. |
| export interface CommandsSkSelectImageEventDetail { |
| id: number; |
| } |
| |
| // Colors to use for gpu op ids |
| const COLORS = [ |
| "#1B9E77", |
| "#D95F02", |
| "#7570B3", |
| "#E7298A", |
| "#66A61E", |
| "#E6AB02", |
| "#A6761D", |
| "#666666", |
| "#09c5d2", |
| "#064f77", |
| "#3a4ce4", |
| "#d256f0", |
| "#feb4c7", |
| "#fa3029", |
| "#ff6821", |
| "#a8ff21", |
| "#a5cf80", |
| "#36d511", |
| "#95f19c", |
| ]; |
| // Commands that increase save/restore depth |
| const INDENTERS: {[key: string]: PrefixItem} = { |
| 'Save': { icon: 'save-icon-sk', color: '#B2DF8A', count: 1 }, |
| 'SaveLayer': { icon: 'content-copy-icon-sk', color: '#FDBF6F', count: 1 }, |
| 'BeginDrawPicture': { icon: 'image-icon-sk', color: '#A6CEE3', count: 1 }, |
| }; |
| // commands that decrease save/restore depth |
| const OUTDENTERS: string[] = ['Restore', 'EndDrawPicture']; |
| |
| export class CommandsSk extends ElementDocSk { |
| private static template = (ele: CommandsSk) => |
| html` |
| <div> |
| ${CommandsSk.filterTemplate(ele)} |
| <div class="horizontal-flex"> |
| <button @click=${ele._opIdFilter} class="short">Show By Op-Id</button> |
| <play-sk .visual=${'full'}></play-sk> |
| </div> |
| <div class="list"> |
| ${ ele._filtered.map((i: number, filtPos: number) => |
| CommandsSk.opTemplate(ele, filtPos, ele._cmd[i])) } |
| </div> |
| </div>`; |
| |
| private static opTemplate = (ele: CommandsSk, filtpos: number, op: Command) => |
| html`<div class="op" id="op-${op.index}" @click=${ |
| (e: MouseEvent) => {ele._clickItem(e, filtpos)}}> |
| <details> |
| <summary class="command-summary ${ ele.position == op.index ? 'selected' : ''}"> |
| <div class="command-icons-group"> |
| <span class="index">${op.index}</span> |
| ${ op.prefixes.map((pre: PrefixItem) => |
| CommandsSk.prefixItemTemplate(ele, pre)) } |
| </div> |
| <div class="command-title">${ op.name }</div> |
| <code class="short-desc">${ op.details.shortDesc }</code> |
| ${ op.range |
| ? html`<button @click=${() => {ele.range = op.range!}} |
| title="Range-filter the command list to this save/restore pair">Zoom</button>` |
| : '' |
| } |
| ${ op.imageIndex |
| ? html`<button @click=${()=>{ele._jumpToImage(op.imageIndex!)}} |
| title="Show the image referenced by this command in the resource viewer" |
| >Image</button>` |
| : '' |
| } |
| <div class="gpu-ops-group"> |
| ${ (op.details.auditTrail && op.details.auditTrail.Ops) |
| ? op.details.auditTrail.Ops.map((gpuOp: SkpJsonGpuOp) => |
| CommandsSk.gpuOpIdTemplate(ele, gpuOp) ) |
| : '' |
| } |
| </div> |
| </summary> |
| <div> |
| <checkbox-sk title="Toggle command visibility" checked=${ op.visible } |
| @change=${ele._toggleVisible(op.index)}></checkbox-sk> |
| <strong>Index: </strong> <span class=index>${op.index}</span> |
| </div> |
| ${ele._renderRullOpRepresentation(ele, op)} |
| </details> |
| </div> |
| <hr>`; |
| |
| private static prefixItemTemplate = (ele: CommandsSk, item: PrefixItem) => |
| html`${ ele._icon(item) } |
| ${ item.count > 1 |
| ? html`<div title="depth of indenting operation" |
| class=count>${ item.count }</div>` |
| : '' |
| }`; |
| |
| private static gpuOpIdTemplate = (ele: CommandsSk, gpuOp: SkpJsonGpuOp) => |
| html`<span title="GPU Op ID - group of commands this was executed with on the GPU" |
| class="gpu-op-id" style="background: ${ ele._gpuOpColor(gpuOp.OpsTaskID) }" |
| >${ gpuOp.OpsTaskID }</span>`; |
| |
| private static filterTemplate = (ele: CommandsSk) => |
| html` |
| <div class="horizontal-flex"> |
| <label title="Filter command names (Single leading ! negates entire filter). |
| Command types can also be filted by clicking on their names in the histogram" |
| >Filter</label> |
| <input @change=${ele._textFilter} value="!DrawAnnotation" |
| id="text-filter"></input> |
| <label>Range</label> |
| <input @change=${ele._rangeInputHandler} class=range-input value="${ ele._range[0] }" |
| id="rangelo"></input> |
| <b>:</b> |
| <input @change=${ele._rangeInputHandler} class=range-input value="${ ele._range[1] }" |
| id="rangehi"></input> |
| <button @click=${ele.clearFilter} id="clear-filter-button">Clear</button> |
| </div>`; |
| |
| // processed command list (no filtering applied). change with processCommands |
| private _cmd: Command[] = []; |
| // list of indices of commands that passed the range and name filters. |
| private _filtered: number[] = []; |
| // position in filtered (visible) command list |
| private _item: number = 0; |
| // range filter |
| private _range: CommandRange = [0, 100]; |
| // counts of command occurances |
| private _histogram: HistogramEntry[] = []; |
| // known command names (set by processCommands) names are lowercased. |
| private _available = new Set<string>(); |
| // subset of command names that should pass the command filter |
| // (names are lowercased) |
| private _includedSet = new Set<string>(); |
| // Play bar submodule |
| private _playSk: PlaySk | null = null; |
| // information about layers collected from commands |
| private _layerInfo: LayerInfo = { |
| uses: new DefaultMap<number, number[]>(() => []), |
| names: new Map<number, string>(), |
| }; |
| |
| // the command count with no filtering |
| get count() { |
| return this._cmd.length; |
| } |
| // the command count with all filters applied |
| get countFiltered() { |
| return this._filtered.length; |
| } |
| |
| get layerInfo(): LayerInfo { |
| return this._layerInfo; |
| } |
| |
| // set the current playback position in the list |
| // (index in filtered list) |
| set item(i: number) { |
| this._item = i; |
| this.querySelector<HTMLDivElement>('#op-' + this._filtered[this._item] |
| )?.scrollIntoView({block: 'nearest'}); |
| this._render(); |
| // notify debugger-page-sk that it needs to draw this.position |
| this.dispatchEvent( |
| new CustomEvent<CommandsSkMovePositionEventDetail>( |
| 'move-command-position', { |
| detail: {position: this.position, paused: this._playSk!.mode === 'pause'}, |
| bubbles: true, |
| })); |
| this._playSk!.movedTo(this._item); |
| } |
| |
| // get the playback index in _cmd after filtering is applied. |
| get position() { |
| return this._filtered[this._item]; |
| } |
| |
| set range(range: CommandRange) { |
| this._range = range; |
| this._applyRangeFilter(); |
| } |
| |
| set textFilter(q: string) { |
| this.querySelector<HTMLInputElement>('#text-filter')!.value = q; |
| if (!this.count) { return; } |
| this._textFilter(); // does render |
| } |
| |
| // Return a list of op indices that pass the current filters. |
| get filtered(): number[] { |
| return this._filtered; |
| } |
| |
| constructor() { |
| super(CommandsSk.template); |
| } |
| |
| connectedCallback() { |
| super.connectedCallback(); |
| this._render(); |
| |
| this._playSk = this.querySelector<PlaySk>('play-sk')!; |
| |
| this._playSk.addEventListener('moveto', (e) => { |
| this.item = (e as CustomEvent<PlaySkMoveToEventDetail>).detail.item; |
| }); |
| |
| this.addDocumentEventListener('toggle-command-inclusion', (e) => { |
| this._toggleName((e as CustomEvent<HistogramSkToggleEventDetail>).detail.name); |
| }); |
| |
| // Jump to a command by it's unfiltered index. |
| this.addDocumentEventListener('jump-command', (e) => { |
| const i = (e as CustomEvent<CommandsSkJumpEventDetail>).detail.unfilteredIndex; |
| const filteredIndex = this._filtered.findIndex(e => e==i); |
| if (filteredIndex !== undefined) { |
| this.item = filteredIndex; |
| } |
| }); |
| } |
| |
| // _processCommands iterates over the commands to extract several things. |
| // * A depth at every command based on Save/Restore pairs. |
| // * A histogram showing how many times each type of command is used. |
| // * A map from layer node ids to the index of any layer use events in the command list. |
| // * The full set of command names that occur |
| processCommands(cmd: SkpJsonCommandList) { |
| const commands: Command[] = []; |
| let depth = 0; |
| const prefixes: PrefixItem[] = []; // A stack of indenting commands |
| // Match up saves and restores, a stack of indices |
| const matchup: number[] = []; |
| // All command types that occur in this frame |
| this._available = new Set<string>(); |
| interface tally { |
| count_in_frame: number, |
| count_in_range_filter: number, |
| } |
| |
| this._layerInfo.uses = new DefaultMap<number, number[]>(() => []); |
| this._layerInfo.names = new Map<number, string>(); |
| |
| // Finds things like "RenderNode(id=10, name='DecorView')" |
| const renderNodeRe = /^RenderNode\(id=([0-9]+), name='([A-Za-z0-9_]+)'\)/; |
| |
| cmd.commands.forEach((com, i) => { |
| const name = com.command; |
| this._available.add(name.toLowerCase()); |
| |
| const out: Command = { |
| index: i, |
| depth: depth, |
| details: com, // unaltered object from json |
| name: name, |
| prefixes: [], |
| visible: true, |
| }; |
| |
| // DrawCommand.cpp will write this field if the command references an image |
| if (com.imageIndex) { |
| out.imageIndex = com.imageIndex; |
| } |
| |
| if (name in INDENTERS) { |
| depth++; |
| |
| matchup.push(i); |
| // If this is the same type of indenting op we've already seen |
| // then just increment the count, otherwise add as a new |
| // op in prefixes. |
| if (depth > 1 && prefixes[prefixes.length-1].icon |
| == INDENTERS[name].icon) { |
| prefixes[prefixes.length-1].count++; |
| } else { |
| prefixes.push(this._copyPrefix(INDENTERS[name])); |
| } |
| } else if (OUTDENTERS.indexOf(name) !== -1) { |
| depth--; |
| |
| // Now that we can match an OUTDENTER with an INDENTER we can set |
| // the _zoom property for both commands. |
| const begin: number = matchup.pop()!; |
| const range = [begin, i] as CommandRange; |
| out.range = range; |
| commands[begin].range = range; |
| |
| // Only pop the op from prefixes if its count has reached 1. |
| if (prefixes[prefixes.length-1].count > 1) { |
| prefixes[prefixes.length-1].count--; |
| } else { |
| prefixes.pop(); |
| } |
| out.depth = depth; |
| } else if (name === 'DrawImageRectLayer') { |
| // A command indicating that a render node with an offscreen buffer (android only) |
| // was drawn as an image. |
| const node = com.layerNodeId!; |
| this._layerInfo.uses.get(node).push(i); |
| } else if (name === 'DrawAnnotation') { |
| // DrawAnnotation is a bit of metadata added by the android view system. |
| // All render nodes have names, but not all of them are drawn with offscreen buffers |
| const annotationKey = com.key; |
| const found = com.key!.match(renderNodeRe); |
| if (found) { |
| // group 1 is the render node id |
| // group 2 is the name of the rendernode. |
| this._layerInfo.names.set(parseInt(found[1]), found[2]); |
| } |
| } |
| |
| // deep copy prefixes because we want a snapshot of the current list and counts |
| out.prefixes = prefixes.map((p: PrefixItem) => this._copyPrefix(p)); |
| |
| commands.push(out); |
| }); |
| |
| this._cmd = commands; |
| this.range = [0, this._cmd.length-1]; // this assignment also triggers render |
| } |
| |
| // User clicked the clear filter button, clear both filters |
| clearFilter() { |
| this.querySelector<HTMLInputElement>('#text-filter')!.value = ''; |
| if (!this.count) { return; } |
| this.range = [0, this._cmd.length-1]; // setter triggers _applyRangeFilter, follow that |
| } |
| |
| // Stop playback and move by a given offset in the filtered list. |
| keyMove(offset: number) { |
| this._playSk!.mode = 'pause'; |
| this.item = Math.max(0, Math.min(this._item + offset, this.countFiltered)); |
| } |
| |
| end() { |
| this.item = this._filtered.length - 1; |
| } |
| |
| private _clickItem(e: MouseEvent, filtIndex: number) { |
| if (this._item !== filtIndex) { |
| // Don't open the dropdown unless you click the already selected item again |
| e.preventDefault(); |
| } |
| this.item = filtIndex; |
| } |
| |
| // filter change coming from histogram |
| private _toggleName(name: string) { |
| const lowerName = name.toLowerCase(); |
| if (!this._available.has(lowerName)) { |
| return; |
| } |
| if (this._includedSet.has(lowerName)) { |
| this._includedSet.delete(lowerName); |
| } else { |
| this._includedSet.add(lowerName); |
| } |
| |
| // represent _includedSet as a negative text filter and put it in the box |
| const diff = new Set(this._available); |
| for (let c of this._includedSet) { |
| diff.delete(c) |
| } |
| let filter = ''; |
| if (diff.size > 0) { |
| filter = '!'+Array.from(diff).join(' '); |
| } |
| this.querySelector<HTMLInputElement>('#text-filter')!.value = filter; |
| // don't trigger _textFilter() since that would send an event back to histogram and |
| // start an infinite loop. this._includedSet is correct, apply it and render. |
| this._applyCommandFilter(); |
| } |
| |
| // Returns a JSON string representation of the command, augmentend with visually rich |
| // or interactive elements for certain types. |
| private _renderRullOpRepresentation(ele: CommandsSk, op: Command) { |
| // Use json.stringify's replacer feature to replace certain objects. |
| // we would like to replace them directly with html templates, but json.stringify |
| // toStrings them, so instead replace them with a magic string and add the template |
| // to a list, then replace those magic strings with items from the list afterwards. |
| |
| // An unlikely string meaning 'insert html template here' |
| const magic = '546rftvyghbjjkjiuytre'; |
| // a list of templates to be used to replaces occurrences of magic. |
| const inserts: TemplateResult[] = []; |
| const replacer = function(name: string, value: any) { |
| if (name === 'imageIndex') { |
| // Show a clickable button that takes the user to the image resource viewer. |
| inserts.push(html`<b>${value}</b> |
| <button @click=${()=>{ele._jumpToImage(value)}} |
| title="Show the image referenced by this command in the resource viewer" |
| >Image</button>`); |
| return magic; |
| } |
| return value; |
| } |
| const strung = JSON.stringify(op.details, replacer, 2); |
| // JSON.stringify adds some quotes around the magic word. |
| // including these in our delimeter removes them. |
| const jsonparts = strung.split('"'+magic+'"'); |
| let result = [html`${jsonparts[0]}`]; |
| for (let i = 1; i < jsonparts.length; i++) { |
| result.push(inserts[i-1]); |
| result.push(html`${jsonparts[i]}`) |
| } |
| return html`<pre>${result}</pre>`; |
| } |
| |
| private _jumpToImage(index: number){ |
| this.dispatchEvent(new CustomEvent<CommandsSkSelectImageEventDetail>( |
| 'select-image', { |
| detail: { id: index }, |
| bubbles: true, |
| })); |
| } |
| |
| // (index is in the unfiltered list) |
| private _toggleVisible(index: number){ |
| this._cmd[index].visible = !this._cmd[index].visible; |
| } |
| |
| // lit-html does not appear to support setting a tag's name with a ${} so here's |
| // a crummy workaround |
| private _icon(item: PrefixItem) { |
| if (item.icon === 'save-icon-sk') { |
| return html`<save-icon-sk style="fill: ${ item.color };" |
| class=icon> </save-icon-sk>`; |
| } else if (item.icon === 'content-copy-icon-sk') { |
| return html`<content-copy-icon-sk style="fill: ${ item.color };" |
| class=icon> </content-copy-icon-sk>`; |
| } else if (item.icon === 'image-icon-sk') { |
| return html`<image-icon-sk style="fill: ${ item.color };" |
| class=icon> </image-icon-sk>`; |
| } |
| } |
| |
| // Any deterministic mapping between integers and colors will do |
| private _gpuOpColor(index: number) { |
| return COLORS[index % COLORS.length]; |
| } |
| |
| // deep copy |
| private _copyPrefix(p: PrefixItem): PrefixItem { |
| return {icon: p.icon, color: p.color, count: p.count}; |
| } |
| |
| private _rangeInputHandler(e: Event) { |
| const lo = parseInt(this.querySelector<HTMLInputElement>('#rangelo')!.value); |
| const hi = parseInt(this.querySelector<HTMLInputElement>('#rangehi')!.value); |
| this.range = [lo, hi]; |
| } |
| |
| // parse the text filter input, and if it is possible to represent it purely as |
| // a command filter, store it in this._includedSet |
| private _textFilter() { |
| let rawFilter = this.querySelector<HTMLInputElement>('#text-filter' |
| )!.value.trim().toLowerCase(); |
| const negative = (rawFilter[0] == '!'); |
| |
| // make sure to copy it so we don't alter this._available |
| this._includedSet = new Set<string>(this._available); |
| |
| if (rawFilter !== '') { |
| if (negative) { |
| rawFilter = rawFilter.slice(1).trim(); |
| const tokens = rawFilter.split(/\s+/); |
| // negative filters can always be represented with histogram selections |
| for (const token of tokens) { |
| this._includedSet.delete(token); |
| } |
| } else { |
| // for positive filters, the text could either be a set of command names, |
| // or a free text search. |
| const tokens = rawFilter.split(/\s+/); |
| this._includedSet = new Set<string>(); |
| for (const token of tokens) { |
| if (this._available.has(token)) { |
| this._includedSet.add(token); |
| } else { |
| // not a command name, bail out, reset this, do a free text search |
| this._includedSet = new Set<string>(this._available); |
| // since we just altered this._includedSet we have to let histogram know. |
| this.dispatchEvent(new CustomEvent<CommandsSkHistogramEventDetail>( |
| 'histogram-update', { |
| detail: { included: new Set<string>(this._includedSet) }, |
| bubbles: true, |
| })); |
| this._freeTextSearch(tokens); |
| // TODO(nifong): need some visual feedback to let the user know |
| console.log(`Query interpreted as free text search because ${token}\ |
| doesn't appear to be a command name`); |
| return; |
| } |
| } |
| } |
| } |
| this.dispatchEvent(new CustomEvent<CommandsSkHistogramEventDetail>( |
| 'histogram-update', { |
| detail: { included: new Set<string>(this._includedSet) }, |
| bubbles: true, |
| })); |
| this._applyCommandFilter(); // note we still do this for emtpy filters. |
| } |
| |
| private _freeTextSearch(tokens: string[]) { |
| // Free text search every command's json representation and include its index in |
| // this._filtered if any token is found |
| const matches = function(s: string) { |
| for (const token of tokens) { |
| if (s.indexOf(token) >= 0) { |
| return true; |
| } |
| } |
| return false; |
| } |
| this._filtered = []; |
| for (let i = this._range[0]; i <= this._range[1]; i++) { |
| const commandText = JSON.stringify(this._cmd[i].details).toLowerCase(); |
| if (matches(commandText)) { |
| this._filtered.push(i); |
| } |
| } |
| this._render(); |
| if (this._filtered.length > 0) { |
| this.item = this._filtered.length - 1; // after render because it causes a scroll |
| } |
| } |
| |
| // Applies range filter and recalculates command name histogram. |
| // The range filter is the first filter applied. The histogram shows any command |
| // that passes |
| // the range filter, and shows a nonzero count for any command that passes the command |
| // filter. |
| private _applyRangeFilter() { |
| // Despite the name, there's not much to "apply" but |
| // the histogram needs to change when the range filter changes which is |
| // why this function is seperate from _applyCommandFilter |
| |
| // Calculate data for histogram |
| // Each command type gets two different counts |
| interface tally { |
| count_in_frame: number, |
| count_in_range_filter: number, |
| } |
| const counts = new DefaultMap<string, tally>(() => ({ |
| count_in_frame: 0, |
| count_in_range_filter: 0 |
| })); |
| for (let i = 0; i < this._cmd.length; i++) { |
| let c = this._cmd[i]; |
| counts.get(c.name).count_in_frame += 1; // always increment first count |
| if (i >= this._range[0] && i <= this._range[1]) { |
| counts.get(c.name)!.count_in_range_filter += 1; // optionally increment filtered count. |
| } |
| } |
| |
| // Now format the histogram as a sorted array suitable for use in the template. |
| // First convert the counts map into an Array of HistogramEntry. |
| this._histogram = []; |
| counts.forEach((value, key) => { |
| this._histogram.push({ |
| name: key, |
| countInFrame: value.count_in_frame, |
| countInRange: value.count_in_range_filter, |
| }) |
| }); |
| // Now sort the array, descending on the rangeCount, ascending |
| // on the op name. |
| // sort by rangeCount so entries don't move on enable/disable |
| this._histogram.sort(function(a,b) { |
| if (a.countInRange == b.countInRange) { |
| if (a.name < b.name) { |
| return -1; |
| } |
| if (a.name > b.name) { |
| return 1; |
| } |
| return 0; |
| } else { |
| return b.countInRange - a.countInRange; |
| } |
| }); |
| |
| // the user's selections are present in the text filter. Apply them now |
| // triggers render |
| this._textFilter(); |
| // that populated this._includedSet, which we also need to notify histogram of. |
| |
| // send this to the histogram element |
| this.dispatchEvent( |
| new CustomEvent<CommandsSkHistogramEventDetail>( |
| 'histogram-update', { |
| detail: { |
| hist: this._histogram, |
| // Make a copy so listener can't accidently write to it. |
| included: new Set<string>(this._includedSet), |
| }, |
| bubbles: true, |
| })); |
| } |
| |
| // Apply a filter specified by this._includedSet and set the filtered list to be visible. |
| private _applyCommandFilter() { |
| // Try to retain the user's playback position in the unfiltered list when doing this |
| // (it is not always possible) |
| const oldPos = this._filtered[this._item]; |
| let newPos: number | null = null; |
| this._filtered = []; |
| for (let i = this._range[0]; i <= this._range[1]; i++) { |
| if (this._includedSet.has(this._cmd[i].name.toLowerCase())) { |
| this._filtered.push(i); |
| if (i === oldPos) { |
| newPos = this._filtered.length - 1; |
| } |
| } |
| } |
| this._playSk!.size = this._filtered.length; |
| this._render(); // gotta render before you can scroll |
| if (newPos !== null) { |
| this.item = newPos; // setter triggers scroll |
| } else { |
| this.item = this._filtered.length - 1; |
| } |
| } |
| |
| // Filters out all but the last command of each gpu op group |
| // Experimental, probably breaks assumptions elsewhere |
| private _opIdFilter() { |
| this._filtered = []; |
| |
| const commandsOfEachOp = new DefaultMap<number, number[]>(() => []); |
| this._cmd.forEach((command, index) => { |
| if (command.details.auditTrail && |
| command.details.auditTrail.Ops) { |
| const opid = command.details.auditTrail.Ops[0].OpsTaskID; |
| commandsOfEachOp.get(opid).push(index); |
| } |
| }); |
| const sortedKeys: number[] = Array.from(commandsOfEachOp.keys()); |
| sortedKeys.sort((a, b) => a - b); // force it to sort as a number, not a string |
| sortedKeys.forEach((k) => { |
| commandsOfEachOp.get(k)!.forEach((i) => { |
| this._filtered.push(i); |
| }); |
| }); |
| |
| |
| this._playSk!.size = this._filtered.length; |
| this.item = this._filtered.length - 1; |
| } |
| } |
| |
| define('commands-sk', CommandsSk); |