From 792fccdcaaf29038e330f42fb78466e8ce0d22b4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 21 Nov 2024 20:14:04 -0800 Subject: [PATCH 1/4] wip --- .vscode/launch.json | 22 ++++++++++++++++++++++ localtypings/pxteditor.d.ts | 2 ++ pxteditor/editorcontroller.ts | 16 +++++++++++++++- pxtlib/cmds.ts | 1 + webapp/src/app.tsx | 4 ++++ webapp/src/cmds.ts | 4 ++++ 6 files changed, 48 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ccb8c126af03..36152f7ff708 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -43,6 +43,28 @@ "sourceMaps": false, "outFiles": [] }, + { + "name": "pxt serve (minecraft)", + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/built/pxt.js", + "stopOnEntry": false, + "args": [ + "serve", + "--noauth" + ], + "cwd": "${workspaceRoot}/../pxt-minecraft", + "runtimeExecutable": null, + "runtimeArgs": [ + "--nolazy" + ], + "env": { + "NODE_ENV": "development" + }, + "console": "integratedTerminal", + "sourceMaps": false, + "outFiles": [] + }, { "name": "pxt serve (microbit)", "type": "node", diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index 26dd274455a3..e6470e7d0300 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -1145,6 +1145,8 @@ declare namespace pxt.editor { blocklyPatch?: (pkgTargetVersion: string, dom: Element) => void; webUsbPairDialogAsync?: (pairAsync: () => Promise, confirmAsync: (options: any) => Promise) => Promise; mkPacketIOWrapper?: (io: pxt.packetio.PacketIO) => pxt.packetio.PacketIOWrapper; + // Post a message to the editor's host application (useful when window.parent.postMessage isn't viable) + postHostMessage?: (msg: pxt.editor.EditorMessageRequest) => void; // Used with the @tutorialCompleted macro. See docs/writing-docs/tutorials.md for more info onTutorialCompleted?: () => void; diff --git a/pxteditor/editorcontroller.ts b/pxteditor/editorcontroller.ts index ca25c008e500..b027f36595c0 100644 --- a/pxteditor/editorcontroller.ts +++ b/pxteditor/editorcontroller.ts @@ -335,8 +335,10 @@ export function bindEditorMessages(getEditorAsync: () => Promise) /** * Sends analytics messages upstream to container if any */ +let controllerAnalyticsEnabled = false; export function enableControllerAnalytics() { - if (!pxt.appTarget.appTheme.allowParentController || !pxt.BrowserUtils.isIFrame()) return; + if (controllerAnalyticsEnabled) return; + if (!pxt.commands.postHostMessage && (!pxt.appTarget.appTheme.allowParentController || !pxt.BrowserUtils.isIFrame())) return; const te = pxt.tickEvent; pxt.tickEvent = function (id: string, data?: pxt.Map): void { @@ -379,6 +381,8 @@ export function enableControllerAnalytics() { data }) } + + controllerAnalyticsEnabled = true; } function sendResponse(request: pxt.editor.EditorMessage, resp: any, success: boolean, error: any) { @@ -424,6 +428,16 @@ export function postHostMessageAsync(msg: pxt.editor.EditorMessageRequest): Prom window.parent.postMessage(env, "*"); } + // Post to editor extension if it wants these messages. + // Note: `postHostMessage` in editor extension does not support responses! Communication on this channel is one-way. + if (pxt.commands.postHostMessage) { + try { + pxt.commands.postHostMessage(msg); + } catch (err) { + pxt.reportException(err); + } + } + if (!msg.response) resolve(undefined) }) diff --git a/pxtlib/cmds.ts b/pxtlib/cmds.ts index ceadae8065d6..52934132efda 100644 --- a/pxtlib/cmds.ts +++ b/pxtlib/cmds.ts @@ -38,6 +38,7 @@ namespace pxt.commands { export let electronDeployAsync: (r: ts.pxtc.CompileResult) => Promise = undefined; // A pointer to the Electron deploy function, so that targets can access it in their extension.ts export let webUsbPairDialogAsync: (pairAsync: () => Promise, confirmAsync: (options: any) => Promise, implicitlyCalled?: boolean) => Promise = undefined; export let onTutorialCompleted: () => void = undefined; + export let postHostMessage: (msg: any /*pxt.editor.EditorMessageRequest*/) => void; export let workspaceLoadedAsync: () => Promise = undefined; export let onMarkdownActivityLoad: (path: string, title?: string, editorProjectName?: string) => Promise = undefined; } \ No newline at end of file diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index eab841f53d32..be2f292cbb47 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -6158,6 +6158,10 @@ document.addEventListener("DOMContentLoaded", async () => { theEditor.setState({ editorState: state }); return initExtensionsAsync(); // need to happen before cmd init }).then(() => cmds.initAsync()) + .then(() => { + // After extension cmds init, call pxteditor.enableControllerAnalytics again in case we need to send analytics to the extension + pxteditor.enableControllerAnalytics(); + }) .then(() => { initPacketIO(); initSerial(); diff --git a/webapp/src/cmds.ts b/webapp/src/cmds.ts index 273c714b031b..d851f27f93de 100644 --- a/webapp/src/cmds.ts +++ b/webapp/src/cmds.ts @@ -390,6 +390,10 @@ function applyExtensionResult() { log(`extension tutorial completed`); pxt.commands.onTutorialCompleted = res.onTutorialCompleted; } + if (res.postHostMessage) { + log(`extension post host message`); + pxt.commands.postHostMessage = res.postHostMessage; + } if (res.showProgramTooLargeErrorAsync) { log(`extension showProgramTooLargeErrorAsync`); pxt.commands.showProgramTooLargeErrorAsync = res.showProgramTooLargeErrorAsync; From f4a7102b8b196768f75df42b3d61329bc2759717 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 5 Dec 2024 17:30:07 -0800 Subject: [PATCH 2/4] telemetry updates for minecraft --- docfiles/pxtweb/cookieCompliance.ts | 71 +++++++++++++++++++++++------ gulpfile.js | 2 + localtypings/pxteditor.d.ts | 7 +-- pxteditor/editorcontroller.ts | 17 +++++-- pxtlib/analytics.ts | 4 +- pxtlib/browserutils.ts | 6 +-- pxtlib/cmds.ts | 4 +- pxtlib/constants.ts | 7 +++ pxtlib/main.ts | 16 +++++-- pxtlib/util.ts | 27 ++++++++++- pxtlib/workeriface.ts | 4 +- webapp/src/app.tsx | 56 +++++++++++++---------- webapp/src/blocks.tsx | 21 +++++---- webapp/src/cmds.ts | 12 ++++- webapp/src/compiler.ts | 5 +- webapp/src/constants.ts | 27 +++++++++++ webapp/src/core.ts | 5 +- webapp/src/marked.tsx | 5 +- webapp/src/simulator.ts | 3 +- webapp/src/workspace.ts | 3 +- 20 files changed, 222 insertions(+), 80 deletions(-) create mode 100644 pxtlib/constants.ts create mode 100644 webapp/src/constants.ts diff --git a/docfiles/pxtweb/cookieCompliance.ts b/docfiles/pxtweb/cookieCompliance.ts index d2da3badfbed..20012a323ddc 100644 --- a/docfiles/pxtweb/cookieCompliance.ts +++ b/docfiles/pxtweb/cookieCompliance.ts @@ -38,20 +38,50 @@ namespace pxt { let eventLogger: TelemetryQueue, Map>; let exceptionLogger: TelemetryQueue>; + type EventListener = (payload: T) => void; + type EventSource = { + subscribe(listener: (payload: T) => void): () => void; + emit(payload: T): void; + }; + + function createEventSource(): EventSource { + const listeners: EventListener[] = []; + + return { + subscribe(listener: EventListener): () => void { + listeners.push(listener); + // Return an unsubscribe function + return () => { + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + }; + }, + emit(payload: T): void { + listeners.forEach(listener => listener(payload)); + } + }; + } + // performance measuring, added here because this is amongst the first (typescript) code ever executed 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 stats: { // name, start, duration - durations: [string, number, number][], + durations: [string, number, number, Map?][], // name, event - milestones: [string, number][] + milestones: [string, number, Map?][] } = { durations: [], milestones: [] } + export function isEnabled() { return enabled; } export let perfReportLogged = false export function splitMs(): number { return Math.round(performance.now() - startTimeMs) @@ -75,8 +105,10 @@ namespace pxt { return prettyStr(splitMs()) } - export function recordMilestone(msg: string, time: number = splitMs()) { - stats.milestones.push([msg, time]) + export function recordMilestone(msg: string, params?: Map) { + const time = splitMs() + stats.milestones.push([msg, time, params]) + onMilestone.emit({ milestone: msg, time, params }); } export function init() { enabled = performance && !!performance.mark && !!performance.measure; @@ -89,7 +121,7 @@ namespace pxt { export function measureStart(name: string) { if (enabled) performance.mark(`${name} start`) } - export function measureEnd(name: string) { + export function measureEnd(name: string, params?: Map) { if (enabled && performance.getEntriesByName(`${name} start`).length) { performance.mark(`${name} end`) performance.measure(`${name} elapsed`, `${name} start`, `${name} end`) @@ -98,7 +130,8 @@ namespace pxt { let measure = e[0] let durMs = measure.duration if (durMs > 10) { - stats.durations.push([name, measure.startTime, durMs]) + stats.durations.push([name, measure.startTime, durMs, params]) + onMeasurement.emit({ name, start: measure.startTime, duration: durMs, params }); } } performance.clearMarks(`${name} start`) @@ -108,34 +141,44 @@ namespace pxt { } export function report(filter: string = null) { perfReportLogged = true; - if (enabled) { - const milestones: {[index: string]: number} = {}; - const durations: {[index: string]: number} = {}; + const milestones: { [index: string]: number } = {}; + const durations: { [index: string]: number } = {}; - let report = `performance report:\n` - for (let [msg, time] of stats.milestones) { + 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}\n` + 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; } } report += `\n` - for (let [msg, start, duration] of stats.durations) { + 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` } durations[msg] = duration; } console.log(report) + enabled = false; // stop collecting milestones and measurements after report return { milestones, durations }; } return undefined; @@ -210,7 +253,7 @@ namespace pxt { // App Insights automatically sends a page view event on setup, but we send our own later with additional properties. // This stops the automatic event from firing, so we don't end up with duplicate page view events. - if(envelope.baseType == "PageviewData" && !envelope.baseData.properties) { + if (envelope.baseType == "PageviewData" && !envelope.baseData.properties) { return false; } diff --git a/gulpfile.js b/gulpfile.js index 007ce4e7bf0c..45ca11761646 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -782,6 +782,8 @@ exports.pxtrunner = gulp.series( pxtembed, ); +exports.pxtweb = pxtweb; +exports.pxtlib = pxtlib; exports.skillmapTest = testSkillmap; exports.updatestrings = updatestrings; exports.lint = lint diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index e6470e7d0300..c90b9b2a7857 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -1145,9 +1145,10 @@ declare namespace pxt.editor { blocklyPatch?: (pkgTargetVersion: string, dom: Element) => void; webUsbPairDialogAsync?: (pairAsync: () => Promise, confirmAsync: (options: any) => Promise) => Promise; mkPacketIOWrapper?: (io: pxt.packetio.PacketIO) => pxt.packetio.PacketIOWrapper; - // Post a message to the editor's host application (useful when window.parent.postMessage isn't viable) - postHostMessage?: (msg: pxt.editor.EditorMessageRequest) => void; - + onPostHostMessage?: (msg: pxt.editor.EditorMessageRequest) => void; + onPerfMilestone?: (payload: { milestone: string, time: number, params?: Map }) => void; + onPerfMeasurement?: (payload: { name: string, start: number, duration: number, params?: Map }) => void; + // Used with the @tutorialCompleted macro. See docs/writing-docs/tutorials.md for more info onTutorialCompleted?: () => void; onMarkdownActivityLoad?: (path: string, title?: string, editorProjectName?: string) => Promise; diff --git a/pxteditor/editorcontroller.ts b/pxteditor/editorcontroller.ts index b027f36595c0..27f43bcf83da 100644 --- a/pxteditor/editorcontroller.ts +++ b/pxteditor/editorcontroller.ts @@ -338,7 +338,14 @@ export function bindEditorMessages(getEditorAsync: () => Promise) let controllerAnalyticsEnabled = false; export function enableControllerAnalytics() { if (controllerAnalyticsEnabled) return; - if (!pxt.commands.postHostMessage && (!pxt.appTarget.appTheme.allowParentController || !pxt.BrowserUtils.isIFrame())) return; + + const hasOnPostHostMessage = !!pxt.commands.onPostHostMessage; + const hasAllowParentController = pxt.appTarget.appTheme.allowParentController; + const isInsideIFrame = pxt.BrowserUtils.isIFrame(); + + if (!(hasOnPostHostMessage || (hasAllowParentController && isInsideIFrame))) { + return; + } const te = pxt.tickEvent; pxt.tickEvent = function (id: string, data?: pxt.Map): void { @@ -428,11 +435,11 @@ export function postHostMessageAsync(msg: pxt.editor.EditorMessageRequest): Prom window.parent.postMessage(env, "*"); } - // Post to editor extension if it wants these messages. - // Note: `postHostMessage` in editor extension does not support responses! Communication on this channel is one-way. - if (pxt.commands.postHostMessage) { + // Post to editor extension if it wants to be notified of these messages. + // Note this is a one-way notification. Responses are not supported. + if (pxt.commands.onPostHostMessage) { try { - pxt.commands.postHostMessage(msg); + pxt.commands.onPostHostMessage(msg); } catch (err) { pxt.reportException(err); } diff --git a/pxtlib/analytics.ts b/pxtlib/analytics.ts index 99546dfaadc3..6685a5d60af8 100644 --- a/pxtlib/analytics.ts +++ b/pxtlib/analytics.ts @@ -53,8 +53,8 @@ namespace pxt.analytics { if (!data) pxt.aiTrackEvent(id); else { - const props: Map = { ...defaultProps } || {}; - const measures: Map = { ...defaultMeasures } || {}; + const props: Map = { ...defaultProps }; + const measures: Map = { ...defaultMeasures }; Object.keys(data).forEach(k => { if (typeof data[k] == "string") props[k] = data[k]; else if (typeof data[k] == "number") measures[k] = data[k]; diff --git a/pxtlib/browserutils.ts b/pxtlib/browserutils.ts index be83e6cace76..88a9a16ab12e 100644 --- a/pxtlib/browserutils.ts +++ b/pxtlib/browserutils.ts @@ -1182,14 +1182,14 @@ namespace pxt.BrowserUtils { } setAsync(filename: string, snippets: Map>, code: string[], highlights: Map>, codeValidationMap: Map>, branch?: string): Promise { - pxt.perf.measureStart("tutorial info db setAsync") + pxt.perf.measureStart(Measurements.TutorialInfoDbSetAsync) const key = getTutorialInfoKey(filename, branch); const hash = getTutorialCodeHash(code); return this.setWithHashAsync(filename, snippets, hash, highlights, codeValidationMap); } setWithHashAsync(filename: string, snippets: Map>, hash: string, highlights: Map>, codeValidationMap: Map>, branch?: string): Promise { - pxt.perf.measureStart("tutorial info db setAsync") + pxt.perf.measureStart(Measurements.TutorialInfoDbSetAsync) const key = getTutorialInfoKey(filename, branch); const blocks: Map = {}; Object.keys(snippets).forEach(hash => { @@ -1210,7 +1210,7 @@ namespace pxt.BrowserUtils { return this.db.setAsync(TutorialInfoIndexedDb.TABLE, entry) .then(() => { - pxt.perf.measureEnd("tutorial info db setAsync") + pxt.perf.measureEnd(Measurements.TutorialInfoDbSetAsync) }) } diff --git a/pxtlib/cmds.ts b/pxtlib/cmds.ts index 52934132efda..7a4b383a2049 100644 --- a/pxtlib/cmds.ts +++ b/pxtlib/cmds.ts @@ -38,7 +38,9 @@ namespace pxt.commands { export let electronDeployAsync: (r: ts.pxtc.CompileResult) => Promise = undefined; // A pointer to the Electron deploy function, so that targets can access it in their extension.ts export let webUsbPairDialogAsync: (pairAsync: () => Promise, confirmAsync: (options: any) => Promise, implicitlyCalled?: boolean) => Promise = undefined; export let onTutorialCompleted: () => void = undefined; - export let postHostMessage: (msg: any /*pxt.editor.EditorMessageRequest*/) => void; + export let onPostHostMessage: (msg: any /*pxt.editor.EditorMessageRequest*/) => void; + 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; export let onMarkdownActivityLoad: (path: string, title?: string, editorProjectName?: string) => Promise = undefined; } \ No newline at end of file diff --git a/pxtlib/constants.ts b/pxtlib/constants.ts new file mode 100644 index 000000000000..4b076d6c3703 --- /dev/null +++ b/pxtlib/constants.ts @@ -0,0 +1,7 @@ +namespace Measurements { + export const TutorialInfoDbSetAsync = "tutorial info db setAsync"; + export const ReloadAppTargetVariant = "reloadAppTargetVariant"; + export const Sha256Buffer = "sha256buffer"; + export const WebworkerRecvHandler = "webworker recvHandler"; + export const NetworkRequest = "network.request"; +} \ No newline at end of file diff --git a/pxtlib/main.ts b/pxtlib/main.ts index 631f1b6f318e..5b7cea1095f8 100644 --- a/pxtlib/main.ts +++ b/pxtlib/main.ts @@ -7,12 +7,20 @@ /// namespace pxt.perf { + export type EventSource = { + subscribe(listener: (payload: T) => void): () => void; + //emit(payload: T): void; // not used externally + }; + // These functions are defined in docfiles/pxtweb/cookieCompliance.ts + export declare function isEnabled(): boolean; export declare let perfReportLogged: boolean; export declare function report(): { milestones: {[index: string]: number}, durations: {[index: string]: number} } | undefined; - export declare function recordMilestone(msg: string, time?: number): void; + export declare function recordMilestone(msg: string, params?: Map): void; export declare function measureStart(name: string): void; - export declare function measureEnd(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 }>; } (function () { // Sometimes these aren't initialized, for example in tests. We only care about them @@ -258,7 +266,7 @@ namespace pxt { } export function reloadAppTargetVariant(temporary = false) { - pxt.perf.measureStart("reloadAppTargetVariant") + pxt.perf.measureStart(Measurements.ReloadAppTargetVariant) const curr = temporary ? "" : JSON.stringify(appTarget); appTarget = U.cloneTargetBundle(savedAppTarget) if (appTargetVariant) { @@ -272,7 +280,7 @@ namespace pxt { // check if apptarget changed if (!temporary && onAppTargetChanged && curr != JSON.stringify(appTarget)) onAppTargetChanged(); - pxt.perf.measureEnd("reloadAppTargetVariant") + pxt.perf.measureEnd(Measurements.ReloadAppTargetVariant) } // this is set by compileServiceVariant in pxt.json diff --git a/pxtlib/util.ts b/pxtlib/util.ts index 79ff25453cf7..c6400075bfee 100644 --- a/pxtlib/util.ts +++ b/pxtlib/util.ts @@ -871,6 +871,11 @@ namespace ts.pxtc.Util { export function requestAsync(options: HttpRequestOptions): Promise { //if (debugHttpRequests) // pxt.debug(`>> ${options.method || "GET"} ${options.url.replace(/[?#].*/, "...")}`); // don't leak secrets in logs + const measureParams: pxt.Map = { + "url": `${encodeURI(options.url.replace(/[?#].*/, "..."))}`, // don't leak secrets in logs + "method": `${options.method || "GET"}` + }; + pxt.perf.measureStart(Measurements.NetworkRequest) return httpRequestCoreAsync(options) .then(resp => { //if (debugHttpRequests) @@ -888,6 +893,24 @@ namespace ts.pxtc.Util { resp.json = U.jsonTryParse(resp.text) return resp }) + .then(resp => { + const contentLength = resp.headers["content-length"]; + if (contentLength) { + measureParams["sizeInBytes"] = `${contentLength}`; + } else if (resp.text) { + if (pxt.perf.isEnabled()) { + // only do this work if perf measurement is actually enabled + const encoder = new TextEncoder(); + const encoded = encoder.encode(resp.text); + measureParams["sizeInBytes"] = encoded.length + ""; + } + } + measureParams["statusCode"] = `${resp.statusCode}`; + return resp + }) + .finally(() => { + pxt.perf.measureEnd(Measurements.NetworkRequest, measureParams) + }) } export function httpGetTextAsync(url: string) { @@ -2003,9 +2026,9 @@ namespace ts.pxtc.BrowserImpl { } export function sha256string(s: string) { - pxt.perf.measureStart("sha256buffer") + pxt.perf.measureStart(Measurements.Sha256Buffer) const res = sha256buffer(Util.toUTF8Array(s)); - pxt.perf.measureEnd("sha256buffer") + pxt.perf.measureEnd(Measurements.Sha256Buffer) return res; } } diff --git a/pxtlib/workeriface.ts b/pxtlib/workeriface.ts index 772bcf26b7df..1311daf854d8 100644 --- a/pxtlib/workeriface.ts +++ b/pxtlib/workeriface.ts @@ -61,9 +61,9 @@ namespace pxt.worker { let worker = new Worker(workerFile) let iface = wrap(v => worker.postMessage(v)) worker.onmessage = ev => { - pxt.perf.measureStart("webworker recvHandler") + pxt.perf.measureStart(Measurements.WebworkerRecvHandler) iface.recvHandler(ev.data) - pxt.perf.measureEnd("webworker recvHandler") + pxt.perf.measureEnd(Measurements.WebworkerRecvHandler) } return iface } diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index be2f292cbb47..c76c6793314c 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -62,6 +62,7 @@ import * as electron from "./electron"; import * as blocklyFieldView from "./blocklyFieldView"; import * as pxtblockly from "../../pxtblocks"; import * as pxteditor from "../../pxteditor"; +import { Measurements, Milestones } from "./constants"; import IAppProps = pxt.editor.IAppProps; import IAppState = pxt.editor.IAppState; @@ -898,7 +899,7 @@ export class ProjectView } private maybeShowPackageErrors(force = false) { - pxt.perf.measureStart("maybeShowPackageErrors") + pxt.perf.measureStart(Measurements.MaybeShowPackageErrors) // Only show in blocks or main.ts if (this.state.currFile) { const fn = this.state.currFile; @@ -930,11 +931,11 @@ export class ProjectView this.openHome(); } }); - pxt.perf.measureEnd("maybeShowPackageErrors") + pxt.perf.measureEnd(Measurements.MaybeShowPackageErrors) return true; } } - pxt.perf.measureEnd("maybeShowPackageErrors") + pxt.perf.measureEnd(Measurements.MaybeShowPackageErrors) return false; } @@ -1850,7 +1851,7 @@ export class ProjectView return; } - pxt.perf.measureStart("loadTutorial loadBlockly") + pxt.perf.measureStart(Measurements.LoadTutorialLoadBlockly) const t = header.tutorial; @@ -1887,7 +1888,7 @@ export class ProjectView core.warningNotification(lf("Could not filter tutorial blocks, displaying full toolbox.")) } finally { - pxt.perf.measureEnd("loadTutorial loadBlockly") + pxt.perf.measureEnd(Measurements.LoadTutorialLoadBlockly) } } @@ -2910,7 +2911,7 @@ export class ProjectView } async createProjectAsync(options: ProjectCreationOptions): Promise { - pxt.perf.measureStart("createProjectAsync") + pxt.perf.measureStart(Measurements.CreateProjectAsync) this.setSideDoc(undefined); if (!options.prj) options.prj = pxt.appTarget.blocksprj; let cfg = pxt.U.clone(options.prj.config); @@ -3003,7 +3004,7 @@ export class ProjectView ); await this.loadHeaderAsync(hd, { filters: options.filters }, false); - pxt.perf.measureEnd("createProjectAsync"); + pxt.perf.measureEnd(Measurements.CreateProjectAsync); } // in multiboard targets, allow use to pick a different board @@ -3277,7 +3278,7 @@ export class ProjectView this.checkForHwVariant() }, pairAsync) .then(() => { - pxt.perf.recordMilestone("HID bridge init finished") + pxt.perf.recordMilestone(Milestones.HIDBridgeInitFinished) }) return true } @@ -3804,7 +3805,7 @@ export class ProjectView } stopSimulator(unload?: boolean, opts?: pxt.editor.SimulatorStartOptions) { - pxt.perf.measureStart("stopSimulator") + pxt.perf.measureStart(Measurements.StopSimulator) pxt.tickEvent('simulator.stop') const clickTrigger = opts && opts.clickTrigger; pxt.debug(`sim: stop (autorun ${this.state.autoRun})`) @@ -3824,7 +3825,7 @@ export class ProjectView this.setState({ simState: SimState.Stopped, autoRun: autoRun }); } - pxt.perf.measureEnd("stopSimulator") + pxt.perf.measureEnd(Measurements.StopSimulator) } suspendSimulator() { @@ -4700,9 +4701,9 @@ export class ProjectView try { if (/^\//.test(path)) { filename = title || path.split('/').reverse()[0].replace('-', ' '); // drop any kind of sub-paths - pxt.perf.measureStart("downloadMarkdown"); + pxt.perf.measureStart(Measurements.DownloadMarkdown); const rawMarkdown = await pxt.Cloud.markdownAsync(path, undefined, undefined, true); - pxt.perf.measureEnd("downloadMarkdown"); + pxt.perf.measureEnd(Measurements.DownloadMarkdown); autoChooseBoard = true; markdown = processMarkdown(rawMarkdown); } else if (scriptId) { @@ -4718,7 +4719,7 @@ export class ProjectView } else if (!!ghid && ghid.owner && ghid.project) { pxt.tickEvent("tutorial.github"); pxt.log(`loading tutorial from ${ghid.fullName}`) - pxt.perf.measureStart("downloadGitHubTutorial"); + pxt.perf.measureStart(Measurements.DownloadGitHubTutorial); const config = await pxt.packagesConfigAsync(); const status = pxt.github.repoStatus(ghid, config); @@ -4734,7 +4735,7 @@ export class ProjectView } let gh = await pxt.github.downloadTutorialMarkdownAsync(path, ghid.tag); - pxt.perf.measureEnd("downloadGitHubTutorial"); + pxt.perf.measureEnd(Measurements.DownloadGitHubTutorial); // check for cached tutorial info, save into IndexedDB if found if (gh?.files[pxt.TUTORIAL_INFO_FILE]) { @@ -4866,7 +4867,7 @@ export class ProjectView async startActivity(opts: pxt.editor.StartActivityOptions) { const { activity, path, editor, title, focus, importOptions, previousProjectHeaderId, carryoverPreviousCode } = opts; - pxt.perf.measureStart("startActivity"); + pxt.perf.measureStart(Measurements.StartActivity); this.textEditor.giveFocusOnLoading = focus; switch (activity) { @@ -4884,7 +4885,7 @@ export class ProjectView break; } - pxt.perf.measureEnd("startActivity") + pxt.perf.measureEnd(Measurements.StartActivity); } async completeTutorialAsync(): Promise { @@ -4979,11 +4980,11 @@ export class ProjectView if (this.isTutorial()) { pxt.tickEvent("tutorial.editorLoaded") this.postTutorialLoaded(); + } - pxt.perf.recordMilestone("editorContentLoaded"); - if (!this.autoRunOnStart()) { - pxt.analytics.trackPerformanceReport(); - } + pxt.perf.recordMilestone(Milestones.EditorContentLoaded); + if (!this.autoRunOnStart()) { + pxt.analytics.trackPerformanceReport(); } const msg: pxt.editor.EditorContentLoadedRequest = { @@ -5892,6 +5893,12 @@ function initExtensionsAsync(): Promise { monacoToolbox.overrideToolbox(res.toolboxOptions.monacoToolbox); } } + if (res.onPerfMilestone) { + pxt.perf.onMilestone.subscribe(res.onPerfMilestone); + } + if (res.onPerfMeasurement) { + pxt.perf.onMeasurement.subscribe(res.onPerfMeasurement); + } cmds.setExtensionResult(res); }); } @@ -5935,7 +5942,7 @@ function filenameForEditor(editor: string): string { } document.addEventListener("DOMContentLoaded", async () => { - pxt.perf.recordMilestone(`DOM loaded`) + pxt.perf.recordMilestone(Milestones.DOMLoaded) pxt.setupWebConfig((window as any).pxtConfig); const config = pxt.webConfig @@ -5965,14 +5972,14 @@ document.addEventListener("DOMContentLoaded", async () => { initGitHubDb(); - pxt.perf.measureStart("setAppTarget"); + pxt.perf.measureStart(Measurements.SetAppTarget); pkg.setupAppTarget((window as any).pxtTargetBundle); // DO NOT put any async code before this line! The serviceworker must be initialized before // the window load event fires appcache.init(() => theEditor.reloadEditor()); pxt.setBundledApiInfo((window as any).pxtTargetBundle.apiInfo); - pxt.perf.measureEnd("setAppTarget"); + pxt.perf.measureEnd(Measurements.SetAppTarget); let theme = pxt.appTarget.appTheme; const isControllerIFrame = theme.allowParentController || pxt.shell.isControllerMode() @@ -6157,7 +6164,8 @@ document.addEventListener("DOMContentLoaded", async () => { if (state) theEditor.setState({ editorState: state }); return initExtensionsAsync(); // need to happen before cmd init - }).then(() => cmds.initAsync()) + }) + .then(() => cmds.initAsync()) .then(() => { // After extension cmds init, call pxteditor.enableControllerAnalytics again in case we need to send analytics to the extension pxteditor.enableControllerAnalytics(); diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx index a8bd9c96d9a1..44180fe0fe44 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -35,6 +35,7 @@ import SimState = pxt.editor.SimState; import { DuplicateOnDragConnectionChecker } from "../../pxtblocks/plugins/duplicateOnDrag"; import { PathObject } from "../../pxtblocks/plugins/renderer/pathObject"; +import { Measurements } from "./constants"; export class Editor extends toolboxeditor.ToolboxEditor { @@ -197,7 +198,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { if (this.delayLoadXml) { if (this.loadingXml) return pxt.debug(`loading blockly`) - pxt.perf.measureStart("domUpdate loadBlockly") + pxt.perf.measureStart(Measurements.DomUpdateLoadBlockly) this.loadingXml = true const loadingDimmer = document.createElement("div"); @@ -235,7 +236,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { } catch { } this.loadingXml = false; this.loadingXmlPromise = null; - pxt.perf.measureEnd("domUpdate loadBlockly") + pxt.perf.measureEnd(Measurements.DomUpdateLoadBlockly) // Do Not Remove: This is used by the skillmap this.parent.onEditorContentLoaded(); }); @@ -597,7 +598,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { } private prepareBlockly(forceHasCategories?: boolean) { - pxt.perf.measureStart("prepareBlockly") + pxt.perf.measureStart(Measurements.PrepareBlockly) let blocklyDiv = document.getElementById('blocksEditor'); if (!blocklyDiv) return; @@ -737,7 +738,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { this.setupIntersectionObserver(); this.resize(); - pxt.perf.measureEnd("prepareBlockly") + pxt.perf.measureEnd(Measurements.PrepareBlockly) } protected setupIntersectionObserver() { @@ -930,7 +931,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { const container = document.getElementById('debuggerToolbox'); if (!container) return; - pxt.perf.measureStart("updateToolbox") + pxt.perf.measureStart(Measurements.UpdateToolbox) const debugging = !!this.parent.state.debugging; let debuggerToolbox = debugging ? ; loadBlocklyAsync() { if (!this._loadBlocklyPromise) { - pxt.perf.measureStart("loadBlockly") + pxt.perf.measureStart(Measurements.LoadBlockly) pxtblockly.applyMonkeyPatches(); this._loadBlocklyPromise = pxt.BrowserUtils.loadBlocklyAsync() .then(() => { @@ -1049,7 +1050,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { this.prepareBlockly(); }) .then(() => initEditorExtensionsAsync()) - .then(() => pxt.perf.measureEnd("loadBlockly")) + .then(() => pxt.perf.measureEnd(Measurements.LoadBlockly)); } return this._loadBlocklyPromise; } @@ -1284,7 +1285,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { private refreshToolbox() { if (!this.blockInfo) return; - pxt.perf.measureStart("refreshToolbox") + pxt.perf.measureStart(Measurements.RefreshToolbox) // no toolbox when readonly if (pxt.shell.isReadOnly()) return; @@ -1334,7 +1335,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { refreshBlockly(); } } - pxt.perf.measureEnd("refreshToolbox") + pxt.perf.measureEnd(Measurements.RefreshToolbox) } filterToolbox(showCategories?: boolean) { diff --git a/webapp/src/cmds.ts b/webapp/src/cmds.ts index d851f27f93de..c036e1cfb268 100644 --- a/webapp/src/cmds.ts +++ b/webapp/src/cmds.ts @@ -390,9 +390,17 @@ function applyExtensionResult() { log(`extension tutorial completed`); pxt.commands.onTutorialCompleted = res.onTutorialCompleted; } - if (res.postHostMessage) { + if (res.onPostHostMessage) { log(`extension post host message`); - pxt.commands.postHostMessage = res.postHostMessage; + pxt.commands.onPostHostMessage = res.onPostHostMessage; + } + if (res.onPerfMilestone) { + log(`extension perf milestone`); + pxt.commands.onPerfMilestone = res.onPerfMilestone; + } + if (res.onPerfMeasurement) { + log(`extension perf measurement`); + pxt.commands.onPerfMeasurement = res.onPerfMeasurement; } if (res.showProgramTooLargeErrorAsync) { log(`extension showProgramTooLargeErrorAsync`); diff --git a/webapp/src/compiler.ts b/webapp/src/compiler.ts index c725a3be9a5c..ea0a6e227307 100644 --- a/webapp/src/compiler.ts +++ b/webapp/src/compiler.ts @@ -6,6 +6,7 @@ import * as pxtblockly from "../../pxtblocks"; import U = pxt.Util; import { postHostMessageAsync, shouldPostHostMessages } from "../../pxteditor"; +import { Measurements } from "./constants"; function setDiagnostics(operation: "compile" | "decompile" | "typecheck", diagnostics: pxtc.KsDiagnostic[], sourceMap?: pxtc.SourceInterval[]) { let mainPkg = pkg.mainEditorPkg(); @@ -1199,7 +1200,7 @@ class ApiInfoIndexedDb { } setAsync(pack: pkg.EditorPackage, apis: pxt.PackageApiInfo): Promise { - pxt.perf.measureStart("compiler db setAsync") + pxt.perf.measureStart(Measurements.CompilerDbSetAsync) const key = getPackageKey(pack); const hash = getPackageHash(pack); @@ -1211,7 +1212,7 @@ class ApiInfoIndexedDb { return this.db.setAsync(ApiInfoIndexedDb.TABLE, entry) .then(() => { - pxt.perf.measureEnd("compiler db setAsync") + pxt.perf.measureEnd(Measurements.CompilerDbSetAsync) }) } diff --git a/webapp/src/constants.ts b/webapp/src/constants.ts new file mode 100644 index 000000000000..4865058db442 --- /dev/null +++ b/webapp/src/constants.ts @@ -0,0 +1,27 @@ +export namespace Milestones { + export const HIDBridgeInitFinished = "HID bridge init finished"; + export const EditorContentLoaded = "editorContentLoaded"; + export const DOMLoaded = "DOM loaded"; + export const LoadingDone = "loading done"; + export const LoadingStarted = "loading started"; + export const SimulatorReady = "simulator ready"; + export const WorkspaceInitFinished = "workspace init finished"; +} + +export namespace Measurements { + export const MaybeShowPackageErrors = "maybeShowPackageErrors"; + export const LoadTutorialLoadBlockly = "loadTutorial loadBlockly"; + export const CreateProjectAsync = "createProjectAsync"; + export const StopSimulator = "stopSimulator"; + export const DownloadMarkdown = "downloadMarkdown"; + export const DownloadGitHubTutorial = "downloadGitHubTutorial"; + export const StartActivity = "startActivity"; + export const SetAppTarget = "setAppTarget"; + export const DomUpdateLoadBlockly = "domUpdate loadBlockly"; + export const PrepareBlockly = "prepareBlockly"; + export const UpdateToolbox = "updateToolbox"; + export const LoadBlockly = "loadBlockly"; + export const RefreshToolbox = "refreshToolbox"; + export const CompilerDbSetAsync = "compiler db setAsync"; + export const RenderMarkdown = "renderMarkdown"; +} diff --git a/webapp/src/core.ts b/webapp/src/core.ts index 8c2d64ab534c..db9d9f5ab6d5 100644 --- a/webapp/src/core.ts +++ b/webapp/src/core.ts @@ -13,6 +13,7 @@ import { pushNotificationMessage } from "../../react-common/components/Notificat import Cloud = pxt.Cloud; import Util = pxt.Util; +import { Milestones } from "./constants"; export type Component = data.Component; @@ -38,7 +39,7 @@ export function isLoading() { export function hideLoading(id: string) { pxt.debug("hideloading: " + id); - pxt.perf.recordMilestone(`loading done #${id}`) + pxt.perf.recordMilestone(Milestones.LoadingDone, { id }) if (loadingQueueMsg[id] != undefined) { // loading exists, remove from queue const index = loadingQueue.indexOf(id); @@ -72,7 +73,7 @@ export function killLoadingQueue() { export function showLoading(id: string, msg: string, percentComplete?: number) { pxt.debug("showloading: " + id); if (loadingQueueMsg[id]) return; // already loading? - pxt.perf.recordMilestone(`loading started #${id}`) + pxt.perf.recordMilestone(Milestones.LoadingStarted, { id }) initializeDimmer(); loadingDimmer.show( "initializing-loader", diff --git a/webapp/src/marked.tsx b/webapp/src/marked.tsx index 589c6541f963..1d4a3edccdb3 100644 --- a/webapp/src/marked.tsx +++ b/webapp/src/marked.tsx @@ -12,6 +12,7 @@ import * as pxtblockly from "../../pxtblocks"; import * as Blockly from "blockly"; import ISettingsProps = pxt.editor.ISettingsProps; +import { Measurements } from "./constants"; interface MarkedContentProps extends ISettingsProps { markdown: string; @@ -653,7 +654,7 @@ export class MarkedContent extends data.Component pubinfo[param] || 'unknown macro') @@ -703,7 +704,7 @@ export class MarkedContent extends data.Component { diff --git a/webapp/src/workspace.ts b/webapp/src/workspace.ts index 16799b22318a..16cf35c131c4 100644 --- a/webapp/src/workspace.ts +++ b/webapp/src/workspace.ts @@ -21,6 +21,7 @@ import Cloud = pxt.Cloud; import * as pxtblockly from "../../pxtblocks"; import { getTextAtTime, HistoryFile } from "../../pxteditor/history"; +import { Milestones } from "./constants"; // Avoid importing entire crypto-js @@ -300,7 +301,7 @@ export function initAsync() { return syncAsync() .then(state => cleanupBackupsAsync().then(() => state)) .then(_ => { - pxt.perf.recordMilestone("workspace init finished") + pxt.perf.recordMilestone(Milestones.WorkspaceInitFinished) return _ }) } From e0558316891e7699c8f96c9f64c47f21cd1b9331 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 5 Dec 2024 19:52:05 -0800 Subject: [PATCH 3/4] stub for tests --- pxtlib/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pxtlib/main.ts b/pxtlib/main.ts index 5b7cea1095f8..bc273f688e47 100644 --- a/pxtlib/main.ts +++ b/pxtlib/main.ts @@ -25,6 +25,8 @@ namespace pxt.perf { (function () { // Sometimes these aren't initialized, for example in tests. We only care about them // doing anything in the browser. + if (!pxt.perf.isEnabled) + pxt.perf.isEnabled = () => false if (!pxt.perf.report) pxt.perf.report = () => undefined if (!pxt.perf.recordMilestone) From 035dbcf78ee6ec2d2ecd30113d36a5db1ec70cc4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 10 Dec 2024 11:27:03 -0800 Subject: [PATCH 4/4] Update docfiles/pxtweb/cookieCompliance.ts Co-authored-by: Thomas Sparks <69657545+thsparks@users.noreply.github.com> --- docfiles/pxtweb/cookieCompliance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docfiles/pxtweb/cookieCompliance.ts b/docfiles/pxtweb/cookieCompliance.ts index 20012a323ddc..eaf5503fc189 100644 --- a/docfiles/pxtweb/cookieCompliance.ts +++ b/docfiles/pxtweb/cookieCompliance.ts @@ -73,7 +73,7 @@ namespace pxt { export let startTimeMs: number; export let stats: { - // name, start, duration + // name, start, duration, params durations: [string, number, number, Map?][], // name, event milestones: [string, number, Map?][]