From 3ae886f967e734387d29d0c0f9acdf9721d35488 Mon Sep 17 00:00:00 2001 From: Svornost <11434-svornost@users.noreply.gitgud.io> Date: Wed, 18 Dec 2019 01:19:52 -0800 Subject: [PATCH] Add a basic profiler to debug options. --- src/debugging/profileInclude.js | 142 ++++++++++++++++++++++++++++++++ src/uncategorized/options.tw | 8 ++ 2 files changed, 150 insertions(+) create mode 100644 src/debugging/profileInclude.js diff --git a/src/debugging/profileInclude.js b/src/debugging/profileInclude.js new file mode 100644 index 00000000000..e686dcb3862 --- /dev/null +++ b/src/debugging/profileInclude.js @@ -0,0 +1,142 @@ +/* This is a terrible hack, but it actually seems to yield decent results, so...yay? */ + +window.ProfileInclude = (function() { + let stack = []; + let tally = new Map(); + let builtinIncludeHandler = null; + + class TallyElement { + constructor(passage) { + this.passage = passage; + this.cumulativeTime = 0.0; + this.calls = 0; + this.children = new Map(); + } + get totalChildTime() { + return Array.from(this.children.values(), (x) => x.cumulativeTime).reduce((sum, current) => { return sum + current; }, 0.0); + } + outputBreakdown() { + if (this.children.size === 0) { + // no breakdown needed if there are no children, so return an empty fragment + return document.createDocumentFragment(); + } + + const selfTime = this.cumulativeTime - this.totalChildTime; + const selfTimePercent = (selfTime / this.cumulativeTime) * 100; + const selfTimePerCall = selfTime / this.calls; + + let childList = document.createElement("ul"); + let selfListItem = document.createElement("li"); + childList.appendChild(selfListItem); + selfListItem.appendChild(document.createTextNode(`<self>: ${selfTime.toFixed(0)}ms (${selfTimePercent.toFixed(2)}%); ${selfTimePerCall.toFixed(0)}ms per call over ${this.calls.toFixed(0)} calls`)); + for (const [, child] of this.children) { + const childTimePercent = (child.cumulativeTime / this.cumulativeTime) * 100; + const childTimePerCall = child.cumulativeTime / child.calls; + let childListItem = document.createElement("li"); + childListItem.appendChild(document.createTextNode(`${child.passage}: ${child.cumulativeTime.toFixed(0)}ms (${childTimePercent.toFixed(2)}%); ${childTimePerCall.toFixed(0)}ms per call over ${child.calls.toFixed(0)} calls`)); + childListItem.appendChild(child.outputBreakdown()); + childList.appendChild(childListItem); + } + return childList; + } + } + + class StackElement { + constructor(passage) { + this.passage = passage; + this.startTime = Date.now(); + } + } + + function Push(passage) { + stack.push(new StackElement(passage)); + } + + function PopAndTally() { + const el = stack.pop(); + const stopTime = Date.now(); + + /* traverse the tally tree, ensuring that it has nodes for every frame still in the stack */ + let parentMap = tally; + for (const child of stack) { + if (!parentMap.has(child.passage)) { + parentMap.set(child.passage, new TallyElement(child.passage)); + } + parentMap = parentMap.get(child.passage).children; + } + + /* add the popped stack frame's time to the correct child of the tally tree node corresponding with the top of the stack */ + if (!parentMap.has(el.passage)) { + parentMap.set(el.passage, new TallyElement(el.passage)); + } + let tallyEl = parentMap.get(el.passage); + tallyEl.cumulativeTime += stopTime - el.startTime; + tallyEl.calls++; + } + + function IncludeBegins(passage) { + Push(passage); + } + + function IncludeEnds() { + PopAndTally(); + } + + $(document).on(':passagestart', function() { + if (V.profiler && !ProfileInclude.IsEnabled()) { + ProfileInclude.Start(); + } else if (!V.profiler && ProfileInclude.IsEnabled()) { + ProfileInclude.Stop(); + } + + tally.clear(); + + if (ProfileInclude.IsEnabled()) { + Push(passage()); // put the "host" passage at the bottom of the stack + } + }); + + $(document).on(':passagedisplay', function() { + if (ProfileInclude.IsEnabled()) { + PopAndTally(); // tally the "host" passage too + + let header = document.createElement("h2"); + header.appendChild(document.createTextNode("Profile Data:")); + let rootList = document.createElement("ul"); + let rootItem = document.createElement("li"); + let rootData = tally.values().next().value; // should be just the one item in the map at this level + rootItem.appendChild(document.createTextNode(`${rootData.passage}: ${rootData.cumulativeTime.toFixed(0)}ms (100.00%)`)); + rootItem.appendChild(rootData.outputBreakdown()); + rootList.appendChild(rootItem); + $("#passages").append(header).append(rootList); + } + }); + + return { + Start: function() { + /* save off the builtin handler for the include macro, delete said macro, then re-register it with instrumentation */ + if (!this.IsEnabled() && Macro.has('include')) { + builtinIncludeHandler = Macro.get('include').handler; + Macro.delete('include'); + Macro.add('include', {handler() { + IncludeBegins(this.args[0]); + builtinIncludeHandler.call(this); + IncludeEnds(); + }}); + } + }, + + IsEnabled: function() { + return (builtinIncludeHandler !== null); + }, + + Stop: function() { + /* restore the original include macro handler */ + if (this.IsEnabled()) { + Macro.delete('include'); + Macro.add('include', {handler: builtinIncludeHandler}); + builtinIncludeHandler = null; + } + } + }; +})(); diff --git a/src/uncategorized/options.tw b/src/uncategorized/options.tw index 20e9c977767..31e723d9ac0 100644 --- a/src/uncategorized/options.tw +++ b/src/uncategorized/options.tw @@ -961,6 +961,14 @@ This save was created using FC version $ver build $releaseID. <br><<link "Reset RA Rules">><<include "Init Rules">><<goto "Rules Assistant">><</link>> </div> + <<options $profiler>> + Passage Profiler is + <<option 1 "Enabled">> + <<option 0 "Disabled">> + <<comment>> + Outputs performance data at the bottom of every passage. + <</options>> + <div class="subHeading"> ''Cheating'' </div> -- GitLab