From 050bc26d3749aed9397de5cff347aa4174e6dbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 4 Jul 2025 18:12:55 +0200 Subject: [PATCH 1/2] Expose a totalMarkerDuration function in console --- .../__snapshots__/window-console.test.js.snap | 5 +- src/test/unit/window-console.test.js | 134 ++++++++++++++++++ src/utils/window-console.js | 27 ++++ 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/src/test/unit/__snapshots__/window-console.test.js.snap b/src/test/unit/__snapshots__/window-console.test.js.snap index 311415926e..6f54d41451 100644 --- a/src/test/unit/__snapshots__/window-console.test.js.snap +++ b/src/test/unit/__snapshots__/window-console.test.js.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`console-accessible values on the window object logs a friendly message 1`] = ` Array [ @@ -28,6 +28,7 @@ Array [ %cwindow.filteredMarkers%c - The current filtered and processed markers %cwindow.selectedMarker%c - The selected processed marker in the current thread %cwindow.callTree%c - The call tree of the current filtered thread +%cwindow.totalMarkerDuration%c - Calculate total duration of a marker array (e.g., totalMarkerDuration(filteredMarkers)) %cwindow.getState%c - The function that returns the current Redux state. %cwindow.selectors%c - All the selectors that are used to get data from the Redux state. %cwindow.dispatch%c - The function to dispatch a Redux action to change the state. @@ -76,6 +77,8 @@ The CallTree class's source code is available here: "", "font-weight: bold;", "", + "font-weight: bold;", + "", "font-style: italic; text-decoration: underline;", "", "font-style: italic; text-decoration: underline;", diff --git a/src/test/unit/window-console.test.js b/src/test/unit/window-console.test.js index a39dbe35a3..95e7a8f7a4 100644 --- a/src/test/unit/window-console.test.js +++ b/src/test/unit/window-console.test.js @@ -73,4 +73,138 @@ describe('console-accessible values on the window object', function () { 1970-01-01 00:00:00.190000000 UTC - [Unknown Process 0: Empty]: D/nsJarProtocol nsJARChannel::nsJARChannel [this=0x87f1ec80] `); }); + + describe('totalMarkerDuration', function () { + let target; + let consoleLogSpy; + + beforeEach(function () { + const store = storeWithSimpleProfile(); + target = {}; + addDataToWindowObject(store.getState, store.dispatch, target); + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(function () { + consoleLogSpy.mockRestore(); + }); + + it('returns 0 for empty array', function () { + const result = target.totalMarkerDuration([]); + expect(result).toBe(0); + }); + + it('returns 0 and logs error for non-array input', function () { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + const result = target.totalMarkerDuration('not an array'); + expect(result).toBe(0); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'totalMarkerDuration expects an array of markers' + ); + consoleErrorSpy.mockRestore(); + }); + + it('calculates duration for interval markers', function () { + const markers = [ + { + start: 100, + end: 200, + name: 'marker1', + category: 0, + data: null, + }, + { + start: 150, + end: 250, + name: 'marker2', + category: 0, + data: null, + }, + ]; + const result = target.totalMarkerDuration(markers); + expect(result).toBe(200); // (200-100) + (250-150) = 100 + 100 = 200 + }); + + it('skips instant markers with null end times', function () { + const markers = [ + { + start: 100, + end: 200, + name: 'interval', + category: 0, + threadId: null, + data: null, + }, + { + start: 150, + end: null, + name: 'instant', + category: 0, + threadId: null, + data: null, + }, + { + start: 300, + end: 400, + name: 'interval2', + category: 0, + threadId: null, + data: null, + }, + ]; + const result = target.totalMarkerDuration(markers); + expect(result).toBe(200); // (200-100) + (400-300) = 100 + 100 = 200 + }); + + it('handles mixed valid and invalid markers', function () { + const markers = [ + { + start: 100, + end: 200, + name: 'valid', + category: 0, + threadId: null, + data: null, + }, + null, + { + start: 'invalid', + end: 300, + name: 'invalid', + category: 0, + threadId: null, + data: null, + }, + { + start: 400, + end: 500, + name: 'valid2', + category: 0, + threadId: null, + data: null, + }, + ]; + const result = target.totalMarkerDuration(markers); + expect(result).toBe(200); // (200-100) + (500-400) = 100 + 100 = 200 + }); + + it('logs formatted duration to console', function () { + const markers = [ + { + start: 100, + end: 350, + name: 'marker', + category: 0, + threadId: null, + data: null, + }, + ]; + target.totalMarkerDuration(markers); + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Total marker duration: 250ms' + ); + }); + }); }); diff --git a/src/utils/window-console.js b/src/utils/window-console.js index 750accc604..8105960bdc 100644 --- a/src/utils/window-console.js +++ b/src/utils/window-console.js @@ -8,6 +8,7 @@ import { selectorsForConsole } from 'firefox-profiler/selectors'; import actions from 'firefox-profiler/actions'; import { shortenUrl } from 'firefox-profiler/utils/shorten-url'; import { createBrowserConnection } from 'firefox-profiler/app-logic/browser-connection'; +import { formatTimestamp } from 'firefox-profiler/utils/format-numbers'; // Despite providing a good libdef for Object.defineProperty, Flow still // special-cases the `value` property: if it's missing it throws an error. Using @@ -263,6 +264,28 @@ export function addDataToWindowObject( return logs.sort().join('\n'); }; + target.totalMarkerDuration = function (markers) { + if (!Array.isArray(markers)) { + console.error('totalMarkerDuration expects an array of markers'); + return 0; + } + + let totalDuration = 0; + for (const marker of markers) { + if ( + marker && + typeof marker.start === 'number' && + typeof marker.end === 'number' + ) { + totalDuration += marker.end - marker.start; + } + // Skip markers with null end times (instant markers have no duration) + } + + console.log(`Total marker duration: ${formatTimestamp(totalDuration)}`); + return totalDuration; + }; + target.shortenUrl = shortenUrl; target.getState = getState; target.selectors = selectorsForConsole; @@ -315,6 +338,7 @@ export function logFriendlyPreamble() { %cwindow.filteredMarkers%c - The current filtered and processed markers %cwindow.selectedMarker%c - The selected processed marker in the current thread %cwindow.callTree%c - The call tree of the current filtered thread + %cwindow.totalMarkerDuration%c - Calculate total duration of a marker array (e.g., totalMarkerDuration(filteredMarkers)) %cwindow.getState%c - The function that returns the current Redux state. %cwindow.selectors%c - All the selectors that are used to get data from the Redux state. %cwindow.dispatch%c - The function to dispatch a Redux action to change the state. @@ -350,6 +374,9 @@ export function logFriendlyPreamble() { // "window.callTree" bold, reset, + // "window.totalMarkerDuration" + bold, + reset, // "window.getState" bold, reset, From ddf6ac302972b7a055ff9d3b9f8983351d443ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Mon, 7 Jul 2025 11:30:42 +0200 Subject: [PATCH 2/2] Address the review comments --- src/test/unit/window-console.test.js | 40 ++++++++++------------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/test/unit/window-console.test.js b/src/test/unit/window-console.test.js index 95e7a8f7a4..dcfee04d60 100644 --- a/src/test/unit/window-console.test.js +++ b/src/test/unit/window-console.test.js @@ -75,26 +75,25 @@ describe('console-accessible values on the window object', function () { }); describe('totalMarkerDuration', function () { - let target; - let consoleLogSpy; + function setup() { + jest.spyOn(console, 'log').mockImplementation(() => {}); - beforeEach(function () { const store = storeWithSimpleProfile(); - target = {}; + const target = {}; addDataToWindowObject(store.getState, store.dispatch, target); - consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - }); - afterEach(function () { - consoleLogSpy.mockRestore(); - }); + return target; + } + beforeEach(function () {}); it('returns 0 for empty array', function () { + const target = setup(); const result = target.totalMarkerDuration([]); expect(result).toBe(0); }); it('returns 0 and logs error for non-array input', function () { + const target = setup(); const consoleErrorSpy = jest .spyOn(console, 'error') .mockImplementation(() => {}); @@ -107,6 +106,7 @@ describe('console-accessible values on the window object', function () { }); it('calculates duration for interval markers', function () { + const target = setup(); const markers = [ { start: 100, @@ -125,9 +125,13 @@ describe('console-accessible values on the window object', function () { ]; const result = target.totalMarkerDuration(markers); expect(result).toBe(200); // (200-100) + (250-150) = 100 + 100 = 200 + + // Make sure that we print a formatted log for the duration. + expect(console.log).toHaveBeenCalledWith('Total marker duration: 200ms'); }); it('skips instant markers with null end times', function () { + const target = setup(); const markers = [ { start: 100, @@ -159,6 +163,7 @@ describe('console-accessible values on the window object', function () { }); it('handles mixed valid and invalid markers', function () { + const target = setup(); const markers = [ { start: 100, @@ -189,22 +194,5 @@ describe('console-accessible values on the window object', function () { const result = target.totalMarkerDuration(markers); expect(result).toBe(200); // (200-100) + (500-400) = 100 + 100 = 200 }); - - it('logs formatted duration to console', function () { - const markers = [ - { - start: 100, - end: 350, - name: 'marker', - category: 0, - threadId: null, - data: null, - }, - ]; - target.totalMarkerDuration(markers); - expect(consoleLogSpy).toHaveBeenCalledWith( - 'Total marker duration: 250ms' - ); - }); }); });