/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; // An outcome of an OptimizationAttempt that is considered successful. const SUCCESSFUL_OUTCOMES = [ "GenericSuccess", "Inlined", "DOM", "Monomorphic", "Polymorphic" ]; /** * Model representing JIT optimization sites from the profiler * for a frame (represented by a FrameNode). Requires optimization data from * a profile, which is an array of RawOptimizationSites. * * When the ThreadNode for the profile iterates over the samples' frames, each * frame's optimizations are accumulated in their respective FrameNodes. Each * FrameNode may contain many different optimization sites. One sample may * pick up optimization X on line Y in the frame, with the next sample * containing optimization Z on line W in the same frame, as each frame is * only function. * * An OptimizationSite contains a record of how many times the * RawOptimizationSite was sampled, as well as the unique id based off of the * original profiler array, and the RawOptimizationSite itself as a reference. * @see devtools/client/performance/modules/logic/tree-model.js * * @struct RawOptimizationSite * A structure describing a location in a script that was attempted to be optimized. * Contains all the IonTypes observed, and the sequence of OptimizationAttempts that * were attempted, and the line and column in the script. This is retrieved from the * profiler after a recording, and our base data structure. Should always be referenced, * and unmodified. * * Note that propertyName is an index into a string table, which needs to be * provided in order for the raw optimization site to be inflated. * * @type {Array} types * @type {Array} attempts * @type {?number} propertyName * @type {number} line * @type {number} column * * * @struct IonType * IonMonkey attempts to classify each value in an optimization site by some type. * Based off of the observed types for a value (like a variable that could be a * string or an instance of an object), it determines what kind of type it should be * classified as. Each IonType here contains an array of all ObservedTypes under `types`, * the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as * `mirType`, and the component of this optimization type that this value refers to -- * like a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b` * (the "Index"). * * Generally the more ObservedTypes, the more deoptimized this OptimizationSite is. * There could be no ObservedTypes, in which case `typeset` is undefined. * * @type {?Array} typeset * @type {string} site * @type {string} mirType * * * @struct ObservedType * When IonMonkey attempts to determine what type a value is, it checks on each sample. * The ObservedType can be thought of in more of JavaScripty-terms, rather than C++. * The `keyedBy` property is a high level description of the type, like "primitive", * "constructor", "function", "singleton", "alloc-site" (that one is a bit more weird). * If the `keyedBy` type is a function or constructor, the ObservedType should have a * `name` property, referring to the function or constructor name from the JS source. * If IonMonkey can determine the origin of this type (like where the constructor is * defined), the ObservedType will also have `location` and `line` properties, but * `location` can sometimes be non-URL strings like "self-hosted" or a memory location * like "102ca7880", or no location at all, and maybe `line` is 0 or undefined. * * @type {string} keyedBy * @type {?string} name * @type {?string} location * @type {?string} line * * * @struct OptimizationAttempt * Each RawOptimizationSite contains an array of OptimizationAttempts. Generally, * IonMonkey goes through a series of strategies for each kind of optimization, starting * from most-niche and optimized, to the less-optimized, but more general strategies -- * for example, a getter opt may first try to optimize for the scenario of a getter on an * `arguments` object -- that will fail most of the time, as most objects are not * arguments objects, but it will attempt several strategies in order until it finds a * strategy that works, or fails. Even in the best scenarios, some attempts will fail * (like the arguments getter example), which is OK, as long as some attempt succeeds * (with the earlier attempts preferred, as those are more optimized). In an * OptimizationAttempt structure, we store just the `strategy` name and `outcome` name, * both from enums in js/public/TrackedOptimizationInfo.h as TRACKED_STRATEGY_LIST and * TRACKED_OUTCOME_LIST, respectively. An array of successful outcome strings are above * in SUCCESSFUL_OUTCOMES. * * @see js/public/TrackedOptimizationInfo.h * * @type {string} strategy * @type {string} outcome */ /* * A wrapper around RawOptimizationSite to record sample count and ID (referring to the * index of where this is in the initially seeded optimizations data), so we don't mutate * the original data from the profiler. Provides methods to access the underlying * optimization data easily, so understanding the semantics of JIT data isn't necessary. * * @constructor * * @param {Array} optimizations * @param {number} optsIndex * * @type {RawOptimizationSite} data * @type {number} samples * @type {number} id */ const OptimizationSite = function (id, opts) { this.id = id; this.data = opts; this.samples = 1; }; /** * Constructor for JITOptimizations. A collection of OptimizationSites for a frame. * * @constructor * @param {Array} rawSites * Array of raw optimization sites. * @param {Array} stringTable * Array of strings from the profiler used to inflate * JIT optimizations. Do not modify this! */ const JITOptimizations = function (rawSites, stringTable) { // Build a histogram of optimization sites. let sites = []; for (let rawSite of rawSites) { let existingSite = sites.find((site) => site.data === rawSite); if (existingSite) { existingSite.samples++; } else { sites.push(new OptimizationSite(sites.length, rawSite)); } } // Inflate the optimization information. for (let site of sites) { let data = site.data; let STRATEGY_SLOT = data.attempts.schema.strategy; let OUTCOME_SLOT = data.attempts.schema.outcome; let attempts = data.attempts.data.map((a) => { return { id: site.id, strategy: stringTable[a[STRATEGY_SLOT]], outcome: stringTable[a[OUTCOME_SLOT]] }; }); let types = data.types.map((t) => { let typeset = maybeTypeset(t.typeset, stringTable); if (typeset) { typeset.forEach(ts => { ts.id = site.id; }); } return { id: site.id, typeset, site: stringTable[t.site], mirType: stringTable[t.mirType] }; }); // Add IDs to to all children objects, so we can correllate sites when // just looking at a specific type, attempt, etc.. attempts.id = types.id = site.id; site.data = { attempts, types, propertyName: maybeString(stringTable, data.propertyName), line: data.line, column: data.column }; } this.optimizationSites = sites.sort((a, b) => b.samples - a.samples); }; /** * Make JITOptimizations iterable. */ JITOptimizations.prototype = { [Symbol.iterator]: function* () { yield* this.optimizationSites; }, get length() { return this.optimizationSites.length; } }; /** * Takes an "outcome" string from an OptimizationAttempt and returns * a boolean indicating whether or not its a successful outcome. * * @return {boolean} */ function isSuccessfulOutcome(outcome) { return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome); } /** * Takes an OptimizationSite. Returns a boolean indicating if the passed * in OptimizationSite has a "good" outcome at the end of its attempted strategies. * * @param {OptimizationSite} optimizationSite * @return {boolean} */ function hasSuccessfulOutcome(optimizationSite) { let attempts = optimizationSite.data.attempts; let lastOutcome = attempts[attempts.length - 1].outcome; return isSuccessfulOutcome(lastOutcome); } function maybeString(stringTable, index) { return index ? stringTable[index] : undefined; } function maybeTypeset(typeset, stringTable) { if (!typeset) { return undefined; } return typeset.map((ty) => { return { keyedBy: maybeString(stringTable, ty.keyedBy), name: maybeString(stringTable, ty.name), location: maybeString(stringTable, ty.location), line: ty.line }; }); } // Map of optimization implementation names to an enum. const IMPLEMENTATION_MAP = { "interpreter": 0, "baseline": 1, "ion": 2 }; const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP); /** * Takes data from a FrameNode and computes rendering positions for * a stacked mountain graph, to visualize JIT optimization tiers over time. * * @param {FrameNode} frameNode * The FrameNode who's optimizations we're iterating. * @param {Array} sampleTimes * An array of every sample time within the range we're counting. * From a ThreadNode's `sampleTimes` property. * @param {number} bucketSize * Size of each bucket in milliseconds. * `duration / resolution = bucketSize` in OptimizationsGraph. * @return {?Array} */ function createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize) { let tierData = frameNode.getTierData(); let stringTable = frameNode._stringTable; let output = []; let implEnum; let tierDataIndex = 0; let nextOptSample = tierData[tierDataIndex]; // Bucket data let samplesInCurrentBucket = 0; let currentBucketStartTime = sampleTimes[0]; let bucket = []; // Store previous data point so we can have straight vertical lines let previousValues; // Iterate one after the samples, so we can finalize the last bucket for (let i = 0; i <= sampleTimes.length; i++) { let sampleTime = sampleTimes[i]; // If this sample is in the next bucket, or we're done // checking sampleTimes and on the last iteration, finalize previous bucket if (sampleTime >= (currentBucketStartTime + bucketSize) || i >= sampleTimes.length) { let dataPoint = {}; dataPoint.values = []; dataPoint.delta = currentBucketStartTime; // Map the opt site counts as a normalized percentage (0-1) // of its count in context of total samples this bucket for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) { dataPoint.values[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1); } // Push the values from the previous bucket to the same time // as the current bucket so we get a straight vertical line. if (previousValues) { let data = Object.create(null); data.values = previousValues; data.delta = currentBucketStartTime; output.push(data); } output.push(dataPoint); // Set the new start time of this bucket and reset its count currentBucketStartTime += bucketSize; samplesInCurrentBucket = 0; previousValues = dataPoint.values; bucket = []; } // If this sample observed an optimization in this frame, record it if (nextOptSample && nextOptSample.time === sampleTime) { // If no implementation defined, it was the "interpreter". implEnum = IMPLEMENTATION_MAP[stringTable[nextOptSample.implementation] || "interpreter"]; bucket[implEnum] = (bucket[implEnum] || 0) + 1; nextOptSample = tierData[++tierDataIndex]; } samplesInCurrentBucket++; } return output; } exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode; exports.OptimizationSite = OptimizationSite; exports.JITOptimizations = JITOptimizations; exports.hasSuccessfulOutcome = hasSuccessfulOutcome; exports.isSuccessfulOutcome = isSuccessfulOutcome; exports.SUCCESSFUL_OUTCOMES = SUCCESSFUL_OUTCOMES;