diff --git a/docfiles/pxtweb/cookieCompliance.ts b/docfiles/pxtweb/cookieCompliance.ts index eaf5503fc189..03c636dd54e5 100644 --- a/docfiles/pxtweb/cookieCompliance.ts +++ b/docfiles/pxtweb/cookieCompliance.ts @@ -38,17 +38,21 @@ namespace pxt { let eventLogger: TelemetryQueue, Map>; let exceptionLogger: TelemetryQueue>; - type EventListener = (payload: T) => void; + type EventListener = (ev: T) => void; type EventSource = { - subscribe(listener: (payload: T) => void): () => void; - emit(payload: T): void; + subscribe(listener: (ev: T) => void): () => void; + emit(ev: T): void; + forEach(callback: (ev: T) => void): void; }; - function createEventSource(): EventSource { + function createEventSource(filter?: ((ev: T) => boolean)): EventSource { + filter = filter || (() => true); const listeners: EventListener[] = []; + const eventCache: T[] = []; return { subscribe(listener: EventListener): () => void { + eventCache.forEach(ev => filter(ev) && listener(ev)); listeners.push(listener); // Return an unsubscribe function return () => { @@ -58,8 +62,12 @@ namespace pxt { } }; }, - emit(payload: T): void { - listeners.forEach(listener => listener(payload)); + emit(ev: T): void { + eventCache.push(ev); + if (filter(ev)) listeners.forEach(listener => listener(ev)); + }, + forEach(callback: (ev: T) => void): void { + eventCache.forEach(ev => filter(ev) && callback(ev)); } }; } @@ -68,18 +76,14 @@ namespace pxt { export namespace perf { let enabled: boolean; - export const onMilestone = createEventSource<{ milestone: string, time: number, params?: Map }>(); - export const onMeasurement = createEventSource<{ name: string, start: number, duration: number, params?: Map }>(); - export let startTimeMs: number; + export let measurementThresholdMs = 10; export let stats: { - // name, start, duration, params - durations: [string, number, number, Map?][], - // name, event - milestones: [string, number, Map?][] + durations: EventSource<{ name: string, start: number, duration: number, params?: Map }>, + milestones: EventSource<{ milestone: string, time: number, params?: Map }>, } = { - durations: [], - milestones: [] + durations: createEventSource((ev) => ev.duration >= measurementThresholdMs), + milestones: createEventSource(), } export function isEnabled() { return enabled; } export let perfReportLogged = false @@ -107,8 +111,7 @@ namespace pxt { export function recordMilestone(msg: string, params?: Map) { const time = splitMs() - stats.milestones.push([msg, time, params]) - onMilestone.emit({ milestone: msg, time, params }); + stats.milestones.emit({ milestone: msg, time, params }); } export function init() { enabled = performance && !!performance.mark && !!performance.measure; @@ -129,17 +132,14 @@ namespace pxt { if (e && e.length === 1) { let measure = e[0] let durMs = measure.duration - if (durMs > 10) { - stats.durations.push([name, measure.startTime, durMs, params]) - onMeasurement.emit({ name, start: measure.startTime, duration: durMs, params }); - } + stats.durations.emit({ name, start: measure.startTime, duration: durMs, params }); } performance.clearMarks(`${name} start`) performance.clearMarks(`${name} end`) performance.clearMeasures(`${name} elapsed`) } } - export function report(filter: string = null) { + export function report() { perfReportLogged = true; if (enabled) { const milestones: { [index: string]: number } = {}; @@ -148,35 +148,29 @@ namespace pxt { let report = `Performance Report:\n` report += `\n` report += `\tMilestones:\n` - for (let [msg, time, params] of stats.milestones) { - if (!filter || msg.indexOf(filter) >= 0) { - let pretty = prettyStr(time) - report += `\t\t${msg} @ ${pretty}` - for (let k of Object.keys(params || {})) { - report += `\n\t\t\t${k}: ${params[k]}` - } - report += `\n` - milestones[msg] = time; + stats.milestones.forEach(({ milestone, time, params }) => { + let pretty = prettyStr(time) + report += `\t\t${milestone} @ ${pretty}` + for (let k of Object.keys(params || {})) { + report += `\n\t\t\t${k}: ${params[k]}` } - } + report += `\n` + milestones[milestone] = time; + }); report += `\n` report += `\tMeasurements:\n` - for (let [msg, start, duration, params] of stats.durations) { - let filterIncl = filter && msg.indexOf(filter) >= 0 - if ((duration > 50 && !filter) || filterIncl) { - let pretty = prettyStr(duration) - report += `\t\t${msg} took ~ ${pretty}` - if (duration > 1000) { - report += ` (${prettyStr(start)} - ${prettyStr(start + duration)})` - for (let k of Object.keys(params || {})) { - report += `\n\t\t\t${k}: ${params[k]}` - } - } - report += `\n` + stats.durations.forEach(({ name, start, duration, params }) => { + let pretty = prettyStr(duration) + report += `\t\t${name} took ~ ${pretty}` + report += ` (${prettyStr(start)} - ${prettyStr(start + duration)})` + for (let k of Object.keys(params || {})) { + report += `\n\t\t\t${k}: ${params[k]}` } - durations[msg] = duration; - } + report += `\n` + durations[name] = duration; + }); + console.log(report) enabled = false; // stop collecting milestones and measurements after report return { milestones, durations }; diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index c90b9b2a7857..d21796d32b67 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -1146,6 +1146,7 @@ declare namespace pxt.editor { webUsbPairDialogAsync?: (pairAsync: () => Promise, confirmAsync: (options: any) => Promise) => Promise; mkPacketIOWrapper?: (io: pxt.packetio.PacketIO) => pxt.packetio.PacketIOWrapper; onPostHostMessage?: (msg: pxt.editor.EditorMessageRequest) => void; + perfMeasurementThresholdMs?: number; onPerfMilestone?: (payload: { milestone: string, time: number, params?: Map }) => void; onPerfMeasurement?: (payload: { name: string, start: number, duration: number, params?: Map }) => void; diff --git a/pxteditor/editorcontroller.ts b/pxteditor/editorcontroller.ts index 27f43bcf83da..ab319e72f525 100644 --- a/pxteditor/editorcontroller.ts +++ b/pxteditor/editorcontroller.ts @@ -439,7 +439,7 @@ export function postHostMessageAsync(msg: pxt.editor.EditorMessageRequest): Prom // Note this is a one-way notification. Responses are not supported. if (pxt.commands.onPostHostMessage) { try { - pxt.commands.onPostHostMessage(msg); + pxt.commands.onPostHostMessage(env); } catch (err) { pxt.reportException(err); } diff --git a/pxtlib/cmds.ts b/pxtlib/cmds.ts index 7a4b383a2049..11655c8ff151 100644 --- a/pxtlib/cmds.ts +++ b/pxtlib/cmds.ts @@ -39,6 +39,7 @@ namespace pxt.commands { export let webUsbPairDialogAsync: (pairAsync: () => Promise, confirmAsync: (options: any) => Promise, implicitlyCalled?: boolean) => Promise = undefined; export let onTutorialCompleted: () => void = undefined; export let onPostHostMessage: (msg: any /*pxt.editor.EditorMessageRequest*/) => void; + export let perfMeasurementThresholdMs: number = undefined; export let onPerfMilestone: (payload: { milestone: string, time: number, params?: Map }) => void = undefined; export let onPerfMeasurement: (payload: { name: string, start: number, duration: number, params?: Map }) => void = undefined; export let workspaceLoadedAsync: () => Promise = undefined; diff --git a/pxtlib/main.ts b/pxtlib/main.ts index 28ece7e82c55..ca948bc25d43 100644 --- a/pxtlib/main.ts +++ b/pxtlib/main.ts @@ -8,8 +8,9 @@ namespace pxt.perf { export type EventSource = { - subscribe(listener: (payload: T) => void): () => void; - //emit(payload: T): void; // not used externally + subscribe(listener: (ev: T) => void): () => void; + //emit(ev: T): void; // not used externally + //forEach(listener: (ev: T) => void): void; // not used externally }; // These functions are defined in docfiles/pxtweb/cookieCompliance.ts @@ -19,8 +20,11 @@ namespace pxt.perf { export declare function recordMilestone(msg: string, params?: Map): void; export declare function measureStart(name: string): void; export declare function measureEnd(name: string, params?: Map): void; - export declare const onMilestone: EventSource<{ milestone: string, time: number, params?: Map }>; - export declare const onMeasurement: EventSource<{ name: string, start: number, duration: number, params?: Map }>; + export declare let measurementThresholdMs: number; + export declare const stats: { + milestones: EventSource<{ milestone: string, time: number, params?: Map }>; + durations: EventSource<{ name: string, start: number, duration: number, params?: Map }>; + }; } (function () { // Sometimes these aren't initialized, for example in tests. We only care about them diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index d497ed60f263..eded843ca335 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -2774,7 +2774,7 @@ export class ProjectView } private editorLoaded() { - pxt.tickEvent('app.editor'); + pxt.tickEvent('app.editor', { projectHeaderId: this.state.header?.id }); } unloadProjectAsync(home?: boolean) { @@ -4982,7 +4982,8 @@ export class ProjectView this.postTutorialLoaded(); } - pxt.perf.recordMilestone(Milestones.EditorContentLoaded); + pxt.perf.recordMilestone(Milestones.EditorContentLoaded, { projectHeaderId: this.state.header?.id }); + if (!this.autoRunOnStart()) { pxt.analytics.trackPerformanceReport(); } @@ -5893,11 +5894,14 @@ function initExtensionsAsync(): Promise { monacoToolbox.overrideToolbox(res.toolboxOptions.monacoToolbox); } } + if (typeof res.perfMeasurementThresholdMs === "number") { + pxt.perf.measurementThresholdMs = res.perfMeasurementThresholdMs; + } if (res.onPerfMilestone) { - pxt.perf.onMilestone.subscribe(res.onPerfMilestone); + pxt.perf.stats.milestones.subscribe(res.onPerfMilestone); } if (res.onPerfMeasurement) { - pxt.perf.onMeasurement.subscribe(res.onPerfMeasurement); + pxt.perf.stats.durations.subscribe(res.onPerfMeasurement); } cmds.setExtensionResult(res); }); diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx index 3ceffa5abb2f..1dc6b34825ef 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -236,7 +236,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { } catch { } this.loadingXml = false; this.loadingXmlPromise = null; - pxt.perf.measureEnd(Measurements.DomUpdateLoadBlockly) + pxt.perf.measureEnd(Measurements.DomUpdateLoadBlockly, { projectHeaderId: this.parent.state.header?.id }); // Do Not Remove: This is used by the skillmap this.parent.onEditorContentLoaded(); }); diff --git a/webapp/src/projects.tsx b/webapp/src/projects.tsx index bb70c6c840b4..4d6cd4fca3a8 100644 --- a/webapp/src/projects.tsx +++ b/webapp/src/projects.tsx @@ -90,7 +90,7 @@ export class Projects extends auth.Component { } chgHeader(hdr: pxt.workspace.Header) { - pxt.tickEvent("projects.header"); + pxt.tickEvent("projects.header", { projectHeaderId: hdr?.id }); core.showLoading("changeheader", lf("loading...")); this.props.parent.loadHeaderAsync(hdr) .catch(e => {