UNPKG

@oaklean/profiler-core

Version:

Part of the @oaklean suite. It provides all basic functions to work with the `.oak` file format. It allows parsing the `.oak` file format as well as tools for analyzing the measurement values. It also provides all necessary capabilities required for prec

186 lines 15.1 kB
"use strict"; /*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.buildModel = void 0; const model_1 = require("../common/model"); const getBestLocation_1 = require("../getBestLocation"); const path_1 = require("../path"); /** * Recursive function that computes and caches the aggregate time for the * children of the computed now. */ const computeAggregateTime = (index, nodes) => { const row = nodes[index]; if (row.aggregateTime) { return row.aggregateTime; } let total = row.selfTime; for (const child of row.children) { total += computeAggregateTime(child, nodes); } return (row.aggregateTime = total); }; /** * Ensures that all profile nodes have a location ID, setting them if they * aren't provided by default. */ const ensureSourceLocations = (profile) => { var _a; if (profile.$vscode) { return profile.$vscode.locations; // profiles we generate are already good } let locationIdCounter = 0; const locationsByRef = new Map(); const getLocationIdFor = (callFrame) => { const ref = [ callFrame.functionName, callFrame.url, callFrame.scriptId, callFrame.lineNumber, callFrame.columnNumber, ].join(':'); const existing = locationsByRef.get(ref); if (existing) { return existing.id; } const id = locationIdCounter++; locationsByRef.set(ref, { id, callFrame, location: { lineNumber: callFrame.lineNumber + 1, columnNumber: callFrame.columnNumber + 1, source: { name: (0, path_1.maybeFileUrlToPath)(callFrame.url), path: (0, path_1.maybeFileUrlToPath)(callFrame.url), sourceReference: 0, }, }, }); return id; }; for (const node of profile.nodes) { node.locationId = getLocationIdFor(node.callFrame); node.positionTicks = (_a = node.positionTicks) === null || _a === void 0 ? void 0 : _a.map(tick => (Object.assign(Object.assign({}, tick), { // weirdly, line numbers here are 1-based, not 0-based. The position tick // only gives line-level granularity, so 'mark' the entire range of source // code the tick refers to startLocationId: getLocationIdFor(Object.assign(Object.assign({}, node.callFrame), { lineNumber: tick.line - 1, columnNumber: 0 })), endLocationId: getLocationIdFor(Object.assign(Object.assign({}, node.callFrame), { lineNumber: tick.line, columnNumber: 0 })) }))); } return [...locationsByRef.values()] .sort((a, b) => a.id - b.id) .map(l => ({ locations: [l.location], callFrame: l.callFrame })); }; /** * Computes the model for the given profile. */ const buildModel = (profile) => { var _a, _b, _c; if (!profile.timeDeltas || !profile.samples) { return { nodes: [], locations: [], samples: profile.samples || [], timeDeltas: profile.timeDeltas || [], rootPath: (_a = profile.$vscode) === null || _a === void 0 ? void 0 : _a.rootPath, duration: profile.endTime - profile.startTime, }; } const { samples } = profile; const timeDeltas = [...profile.timeDeltas]; const sourceLocations = ensureSourceLocations(profile); const locations = sourceLocations.map((l, id) => { const src = (0, getBestLocation_1.getBestLocation)(profile, l.locations); return { id, selfTime: 0, aggregateTime: 0, ticks: 0, category: (0, model_1.categorize)(l.callFrame, src), callFrame: l.callFrame, src, }; }); const idMap = new Map(); const mapId = (nodeId) => { let id = idMap.get(nodeId); if (id === undefined) { id = idMap.size; idMap.set(nodeId, id); } return id; }; // 1. Created a sorted list of nodes. It seems that the profile always has // incrementing IDs, although they are just not initially sorted. const nodes = new Array(profile.nodes.length); for (let i = 0; i < profile.nodes.length; i++) { const node = profile.nodes[i]; // make them 0-based: const id = mapId(node.id); nodes[id] = { id, selfTime: 0, aggregateTime: 0, locationId: node.locationId, children: ((_b = node.children) === null || _b === void 0 ? void 0 : _b.map(mapId)) || [], }; for (const child of node.positionTicks || []) { if (child.startLocationId) { locations[child.startLocationId].ticks += child.ticks; } } } for (const node of nodes) { for (const child of node.children) { nodes[child].parent = node.id; } } // 2. The profile samples are the 'bottom-most' node, the currently running // code. Sum of these in the self time. const duration = profile.endTime - profile.startTime; let lastNodeTime = duration - timeDeltas[0]; for (let i = 0; i < timeDeltas.length - 1; i++) { const d = timeDeltas[i + 1]; nodes[mapId(samples[i])].selfTime += d; lastNodeTime -= d; } // Add in an extra time delta for the last sample. `timeDeltas[0]` is the // time before the first sample, and the time of the last sample is only // derived (approximately) by the missing time in the sum of deltas. Save // some work by calculating it here. if (nodes.length) { nodes[mapId(samples[timeDeltas.length - 1])].selfTime += lastNodeTime; timeDeltas.push(lastNodeTime); } // 3. Add the aggregate times for all node children and locations const calcAggregatedTimeOfLocations = (node, visited) => { const location = locations[node.locationId]; let selfAdded = false; if (!visited.has(node.locationId)) { // node is not an ancestor of a node with the same location visited.add(node.locationId); selfAdded = true; location.aggregateTime += computeAggregateTime(node.id, nodes); } location.selfTime += node.selfTime; for (const child of node.children) { calcAggregatedTimeOfLocations(nodes[child], visited); } if (selfAdded) { visited.delete(node.locationId); } }; calcAggregatedTimeOfLocations(nodes[0], new Set()); return { nodes, locations, samples: samples.map(mapId), timeDeltas, rootPath: (_c = profile.$vscode) === null || _c === void 0 ? void 0 : _c.rootPath, duration, }; }; exports.buildModel = buildModel; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibW9kZWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9saWIvdnNjb2RlLWpzLXByb2ZpbGUtY29yZS9zcmMvY3B1L21vZGVsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7NERBRTREOzs7QUFHNUQsMkNBQW9EO0FBRXBELHdEQUFxRDtBQUVyRCxrQ0FBNkM7QUFnRDdDOzs7R0FHRztBQUNILE1BQU0sb0JBQW9CLEdBQUcsQ0FBQyxLQUFhLEVBQUUsS0FBc0IsRUFBVSxFQUFFO0lBQzdFLE1BQU0sR0FBRyxHQUFHLEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN6QixJQUFJLEdBQUcsQ0FBQyxhQUFhLEVBQUUsQ0FBQztRQUN0QixPQUFPLEdBQUcsQ0FBQyxhQUFhLENBQUM7SUFDM0IsQ0FBQztJQUVELElBQUksS0FBSyxHQUFHLEdBQUcsQ0FBQyxRQUFRLENBQUM7SUFDekIsS0FBSyxNQUFNLEtBQUssSUFBSSxHQUFHLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDakMsS0FBSyxJQUFJLG9CQUFvQixDQUFDLEtBQUssRUFBRSxLQUFLLENBQUMsQ0FBQztJQUM5QyxDQUFDO0lBRUQsT0FBTyxDQUFDLEdBQUcsQ0FBQyxhQUFhLEdBQUcsS0FBSyxDQUFDLENBQUM7QUFDckMsQ0FBQyxDQUFDO0FBRUY7OztHQUdHO0FBQ0gsTUFBTSxxQkFBcUIsR0FBRyxDQUFDLE9BQXVCLEVBQXNDLEVBQUU7O0lBQzVGLElBQUksT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ3BCLE9BQU8sT0FBTyxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyx3Q0FBd0M7SUFDNUUsQ0FBQztJQUVELElBQUksaUJBQWlCLEdBQUcsQ0FBQyxDQUFDO0lBQzFCLE1BQU0sY0FBYyxHQUFHLElBQUksR0FBRyxFQUczQixDQUFDO0lBRUosTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLFNBQWdDLEVBQUUsRUFBRTtRQUM1RCxNQUFNLEdBQUcs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