diff --git a/profiler-cli/guide.txt b/profiler-cli/guide.txt index 4b401645db..e6e06749b4 100644 --- a/profiler-cli/guide.txt +++ b/profiler-cli/guide.txt @@ -95,6 +95,7 @@ CORE WORKFLOW profiler-cli profile logs --search "connect" Filter by substring in message profiler-cli profile logs --thread t-0 Restrict to a specific thread profiler-cli profile logs --limit 200 Show only the first 200 entries + profiler-cli profile logs --limit 0 All entries (no limit) Step 6: Drill into specifics profiler-cli marker info m-1234 Full details for a marker (from handles in marker list) @@ -343,6 +344,7 @@ COMMON ANALYSIS PATTERNS profiler-cli thread markers --search "Paint" --list | head -50 profiler-cli thread markers --search "GC" --min-duration 5 --list profiler-cli thread markers --list --limit 100 First 100 markers in chronological order + profiler-cli thread markers --list --limit 0 Every matching marker (no limit; can be very long) Investigate a page load: profiler-cli thread page-load Overview: milestones, top resources, CPU categories, jank diff --git a/profiler-cli/src/commands/profile.ts b/profiler-cli/src/commands/profile.ts index ad28f60002..b9310f6c44 100644 --- a/profiler-cli/src/commands/profile.ts +++ b/profiler-cli/src/commands/profile.ts @@ -7,7 +7,8 @@ */ import type { Command } from 'commander'; -import { addGlobalOptions, parseIntArg, runCommand } from './shared'; +import { parseLimitArg } from '../utils/parse'; +import { addGlobalOptions, runCommand } from './shared'; export function registerProfileCommand( program: Command, @@ -69,7 +70,7 @@ export function registerProfileCommand( `Minimum log level: ${VALID_LOG_LEVELS.join(', ')}` ) .option('--search ', 'Filter by substring in message') - .option('--limit ', 'Limit to first N entries') + .option('--limit ', 'Limit to first N entries (0 = no limit)') ).action(async (opts) => { if (opts.level !== undefined && !VALID_LOG_LEVELS.includes(opts.level)) { console.error( @@ -78,10 +79,7 @@ export function registerProfileCommand( process.exit(1); } - let limit: number | undefined; - if (opts.limit !== undefined) { - limit = parseIntArg('--limit', opts.limit, 1); - } + const limit = parseLimitArg('--limit', opts.limit); const hasFilters = opts.thread !== undefined || diff --git a/profiler-cli/src/commands/thread.ts b/profiler-cli/src/commands/thread.ts index 85882f02e7..dcc267f7c2 100644 --- a/profiler-cli/src/commands/thread.ts +++ b/profiler-cli/src/commands/thread.ts @@ -7,7 +7,7 @@ */ import type { Command } from 'commander'; -import { parseEphemeralFilters } from '../utils/parse'; +import { parseEphemeralFilters, parseLimitArg } from '../utils/parse'; import { addGlobalOptions, addSampleFilterOptions, @@ -206,7 +206,7 @@ export function registerThreadCommand( 'Filter by maximum duration in milliseconds' ) .option('--has-stack', 'Show only markers with stack traces') - .option('--limit ', 'Limit the number of results shown') + .option('--limit ', 'Limit the number of results shown (0 = no limit)') .option( '--group-by ', 'Group by custom keys (e.g. "type,name" or "type,field:eventType")' @@ -297,9 +297,7 @@ Examples: 'Error: --max-duration must be a positive number (in milliseconds)' ); } - if (opts.limit !== undefined) { - markerFilters.limit = parseIntArg('--limit', opts.limit, 1); - } + markerFilters.limit = parseLimitArg('--limit', opts.limit); if (opts.topN !== undefined) { markerFilters.topN = parseIntArg('--top-n', opts.topN, 1); } @@ -332,7 +330,7 @@ Examples: '--max-duration ', 'Filter by maximum total request duration in milliseconds' ) - .option('--limit ', 'Max requests to show (default: 20, 0 = show all)') + .option('--limit ', 'Max requests to show (default: 20, 0 = no limit)') .option( '--sort ', 'Sort requests by "duration" (default, slowest first) or "start" (chronological)' @@ -375,12 +373,7 @@ Examples: ); } if (opts.limit !== undefined) { - networkFilters.limit = parseIntArg( - '--limit', - opts.limit, - 0, - 'Error: --limit must be a non-negative integer (0 = show all)' - ); + networkFilters.limit = parseLimitArg('--limit', opts.limit) ?? 0; } else { networkFilters.limit = 20; } @@ -426,12 +419,10 @@ Examples: ); } if (opts.jankLimit !== undefined) { - pageLoadOptions.jankLimit = parseIntArg( - '--jank-limit', - opts.jankLimit, - 0, - 'Error: --jank-limit must be a non-negative integer (0 = show all)' - ); + // `collectPageLoad` does `jankLimit ?? 10`, so forwarding `undefined` + // here would silently reinstate that default; 0 is its show-all sentinel. + pageLoadOptions.jankLimit = + parseLimitArg('--jank-limit', opts.jankLimit) ?? 0; } await runCommand( @@ -458,7 +449,10 @@ Examples: '--min-self ', 'Filter by minimum self time percentage' ) - .option('--limit ', 'Limit the number of results shown') + .option( + '--limit ', + 'Limit the number of results shown (0 = no limit)' + ) .option('--include-idle', 'Include idle samples in percentages') ) ).action(async (opts) => { @@ -482,9 +476,7 @@ Examples: 'Error: --min-self must be a number between 0 and 100 (percentage)' ); } - if (opts.limit !== undefined) { - functionFilters.limit = parseIntArg('--limit', opts.limit, 1); - } + functionFilters.limit = parseLimitArg('--limit', opts.limit); } const sampleFilters = parseEphemeralFilters(opts); diff --git a/profiler-cli/src/formatters.ts b/profiler-cli/src/formatters.ts index 11557e19a9..423d68558d 100644 --- a/profiler-cli/src/formatters.ts +++ b/profiler-cli/src/formatters.ts @@ -1157,6 +1157,15 @@ export function formatThreadMarkersResult( ` ${m.handle.padEnd(8)} ${m.name.padEnd(30)} ${startStr.padEnd(14)} ${durationStr.padEnd(10)} ${stackIndicator}${labelSuffix}` ); } + // Truncating a chronological list silently would hide the tail of the + // timeline, so always say how much was dropped and how to get it back. + const omitted = result.filteredMarkerCount - result.flatMarkers.length; + if (omitted > 0) { + lines.push( + `\n ... (${omitted} more markers omitted: showing the first ${result.flatMarkers.length} of ${result.filteredMarkerCount})`, + ' Use --limit 0 to list all of them, or --limit for a larger window.' + ); + } return lines.join('\n'); } @@ -1207,7 +1216,12 @@ export function formatThreadMarkersResult( } if (result.byType.length > 15) { - lines.push(` ... (${result.byType.length - 15} more marker names)`); + // This list is capped at 15 by the formatter, not by --limit, so point at + // what does show the rest rather than leaving a dead end. + lines.push( + ` ... (${result.byType.length - 15} more marker names: showing the top 15 of ${result.byType.length})`, + ' Use --json for every marker name, or --search to narrow to one.' + ); } lines.push(''); @@ -1394,12 +1408,15 @@ export function formatThreadFunctionsResult( if (result.filteredFunctionCount > result.functions.length) { const omittedCount = result.filteredFunctionCount - result.functions.length; - lines.push(`\n ... (${omittedCount} more functions omitted)`); + lines.push( + `\n ... (${omittedCount} more functions omitted: showing the first ${result.functions.length} of ${result.filteredFunctionCount})`, + ' Use --limit 0 to list all of them, or --limit for a larger window.' + ); } lines.push(''); lines.push( - 'Use --search , --min-self , or --limit to filter functions, or f- handles to inspect individual functions.' + 'Use --search , --min-self , or --limit (0 = no limit) to filter functions, or f- handles to inspect individual functions.' ); return lines.join('\n'); @@ -1864,7 +1881,10 @@ export function formatProfileLogsResult( } if (isFiltered && shown < total) { - lines.push(`Showing ${shown} of ${total} log entries (filtered/limited)`); + lines.push( + `Showing the first ${shown} of ${total} log entries — ${total - shown} omitted by --limit ${shown}.`, + 'Use --limit 0 to print all of them, or --limit for a larger window.' + ); } else if (isFiltered) { lines.push(`${total} log entries (filtered)`); } else { diff --git a/profiler-cli/src/test/fixtures/limit-boundaries.json b/profiler-cli/src/test/fixtures/limit-boundaries.json new file mode 100644 index 0000000000..85f76b164c --- /dev/null +++ b/profiler-cli/src/test/fixtures/limit-boundaries.json @@ -0,0 +1,1130 @@ +{ + "meta": { + "abi": "x86_64-gcc3", + "interval": 1, + "misc": "rv:48.0", + "oscpu": "Intel Mac OS X 10.11", + "platform": "Macintosh", + "processType": 0, + "product": "Firefox", + "stackwalk": 1, + "startTime": 1460221352723.438, + "toolkit": "cocoa", + "version": 4, + "preprocessedProfileVersion": 7 + }, + "threads": [ + { + "name": "GeckoMain", + "processType": "default", + "pausedRanges": [], + "processStartupTime": 0, + "processShutdownTime": null, + "registerTime": 0, + "unregisterTime": null, + "libs": [ + { + "breakpadId": "F1D957D30B413D55A539BBA06F90DD8F0", + "arch": "x86_64", + "debugPath": "", + "debugName": "firefox", + "name": "firefox", + "path": "/Applications/FirefoxNightly.app/Contents/MacOS/firefox", + "start": 4294967296, + "end": 4294977296 + }, + { + "breakpadId": "1000000000000000000000000000000A1", + "arch": "x86_64", + "debugPath": "", + "debugName": "examplebinary", + "name": "examplebinary", + "path": "/tmp/examplebinary", + "start": 8589934592, + "end": 8589934612 + }, + { + "breakpadId": "100000000000000000000000000000A27", + "arch": "x86_64", + "debugPath": "", + "debugName": "examplebinary2.pdb", + "name": "examplebinary2", + "path": "C:\\examplebinary2", + "start": 8589934612, + "end": 8589934632 + } + ], + "frameTable": { + "length": 5, + "implementation": [null, null, null, null, 6], + "optimizations": [null, null, null, null, null], + "line": [null, null, null, 4391, 34], + "category": [null, null, null, 16, null], + "func": [0, 1, 2, 3, 4], + "address": [-1, 3972, 6725, -1, -1] + }, + "funcTable": { + "length": 5, + "name": [0, 1, 2, 3, 13], + "resource": [-1, 0, 0, -1, 1], + "address": [-1, 3972, 6725, -1, -1], + "isJS": [false, false, false, false, true], + "fileName": [null, null, null, null, 12], + "lineNumber": [null, null, null, null, 34] + }, + "resourceTable": { + "length": 2, + "type": [1, 5], + "name": [11, 12], + "lib": [0], + "icon": [], + "addonId": [], + "host": [] + }, + "stackTable": { + "length": 5, + "prefix": [null, 0, 1, 1, 1], + "frame": [0, 1, 2, 3, 4] + }, + "markers": { + "length": 59, + "name": [ + 4, 5, 10, 10, 5, 8, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66 + ], + "time": [ + 0, 2, 4, 5, 8, 9, 11, 13, 14, 13, 23, 33, 43, 53, 63, 73, 83, 93, 103, + 113, 123, 133, 143, 153, 163, 173, 183, 193, 203, 213, 223, 233, 243, + 0, 0, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105, 115, 125, 135, 145, + 155, 165, 175, 185, 195, 205, 215, 225, 235, 245 + ], + "data": [ + { "category": "VsyncTimestamp", "vsync": 0 }, + { + "category": "Paint", + "interval": "start", + "stack": { + "markers": { + "schema": { "name": 0, "time": 1, "data": 2 }, + "data": [] + }, + "name": "SyncProfile", + "samples": { + "schema": { + "stack": 0, + "time": 1, + "responsiveness": 2, + "rss": 3, + "uss": 4, + "frameNumber": 5, + "power": 6 + }, + "data": [[2, 1]] + } + }, + "type": "tracing" + }, + { "category": "Paint", "interval": "start", "type": "tracing" }, + { "category": "Paint", "interval": "end", "type": "tracing" }, + { "category": "Paint", "interval": "end", "type": "tracing" }, + { + "type": "DOMEvent", + "startTime": 9, + "endTime": 10, + "eventType": "mouseout", + "phase": 3 + }, + { "startTime": 11, "endTime": 12 }, + { + "type": "Network", + "startTime": 3, + "endTime": 13, + "id": 388634410746504, + "status": "STATUS_STOP", + "pri": -20, + "count": 37838, + "URI": "https://github.com/rustwasm/wasm-bindgen/issues/3", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "DiskIO", + "startTime": 13.5, + "endTime": 14, + "operation": "stat", + "source": "NSPRIOInterposer", + "filename": "", + "cause": { "time": 13.5, "stack": 0 } + }, + { + "type": "Network", + "startTime": 13, + "endTime": 19, + "id": 1001, + "status": "STATUS_STOP", + "pri": -20, + "count": 1001, + "URI": "https://example.com/asset-01.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 23, + "endTime": 30, + "id": 1002, + "status": "STATUS_STOP", + "pri": -20, + "count": 1002, + "URI": "https://example.com/asset-02.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 33, + "endTime": 41, + "id": 1003, + "status": "STATUS_STOP", + "pri": -20, + "count": 1003, + "URI": "https://example.com/asset-03.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 43, + "endTime": 52, + "id": 1004, + "status": "STATUS_STOP", + "pri": -20, + "count": 1004, + "URI": "https://example.com/asset-04.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 53, + "endTime": 63, + "id": 1005, + "status": "STATUS_STOP", + "pri": -20, + "count": 1005, + "URI": "https://example.com/asset-05.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 63, + "endTime": 74, + "id": 1006, + "status": "STATUS_STOP", + "pri": -20, + "count": 1006, + "URI": "https://example.com/asset-06.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 73, + "endTime": 85, + "id": 1007, + "status": "STATUS_STOP", + "pri": -20, + "count": 1007, + "URI": "https://example.com/asset-07.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 83, + "endTime": 96, + "id": 1008, + "status": "STATUS_STOP", + "pri": -20, + "count": 1008, + "URI": "https://example.com/asset-08.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 93, + "endTime": 107, + "id": 1009, + "status": "STATUS_STOP", + "pri": -20, + "count": 1009, + "URI": "https://example.com/asset-09.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 103, + "endTime": 118, + "id": 1010, + "status": "STATUS_STOP", + "pri": -20, + "count": 1010, + "URI": "https://example.com/asset-10.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 113, + "endTime": 129, + "id": 1011, + "status": "STATUS_STOP", + "pri": -20, + "count": 1011, + "URI": "https://example.com/asset-11.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 123, + "endTime": 140, + "id": 1012, + "status": "STATUS_STOP", + "pri": -20, + "count": 1012, + "URI": "https://example.com/asset-12.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 133, + "endTime": 151, + "id": 1013, + "status": "STATUS_STOP", + "pri": -20, + "count": 1013, + "URI": "https://example.com/asset-13.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 143, + "endTime": 162, + "id": 1014, + "status": "STATUS_STOP", + "pri": -20, + "count": 1014, + "URI": "https://example.com/asset-14.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 153, + "endTime": 173, + "id": 1015, + "status": "STATUS_STOP", + "pri": -20, + "count": 1015, + "URI": "https://example.com/asset-15.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 163, + "endTime": 184, + "id": 1016, + "status": "STATUS_STOP", + "pri": -20, + "count": 1016, + "URI": "https://example.com/asset-16.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 173, + "endTime": 195, + "id": 1017, + "status": "STATUS_STOP", + "pri": -20, + "count": 1017, + "URI": "https://example.com/asset-17.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 183, + "endTime": 206, + "id": 1018, + "status": "STATUS_STOP", + "pri": -20, + "count": 1018, + "URI": "https://example.com/asset-18.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 193, + "endTime": 217, + "id": 1019, + "status": "STATUS_STOP", + "pri": -20, + "count": 1019, + "URI": "https://example.com/asset-19.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 203, + "endTime": 228, + "id": 1020, + "status": "STATUS_STOP", + "pri": -20, + "count": 1020, + "URI": "https://example.com/asset-20.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 213, + "endTime": 239, + "id": 1021, + "status": "STATUS_STOP", + "pri": -20, + "count": 1021, + "URI": "https://example.com/asset-21.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 223, + "endTime": 250, + "id": 1022, + "status": "STATUS_STOP", + "pri": -20, + "count": 1022, + "URI": "https://example.com/asset-22.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 233, + "endTime": 261, + "id": 1023, + "status": "STATUS_STOP", + "pri": -20, + "count": 1023, + "URI": "https://example.com/asset-23.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Network", + "startTime": 243, + "endTime": 272, + "id": 1024, + "status": "STATUS_STOP", + "pri": -20, + "count": 1024, + "URI": "https://example.com/asset-24.js", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + }, + { + "type": "Text", + "name": "Document https://example.com/ loaded after 400ms", + "startTime": 0, + "endTime": 400, + "innerWindowID": 1 + }, + { + "type": "Text", + "category": "Navigation", + "startTime": 0, + "endTime": 200, + "innerWindowID": 1 + }, + { "type": "Text", "name": "Jank 0", "startTime": 5, "endTime": 7 }, + { "type": "Text", "name": "Jank 1", "startTime": 15, "endTime": 17 }, + { "type": "Text", "name": "Jank 2", "startTime": 25, "endTime": 27 }, + { "type": "Text", "name": "Jank 3", "startTime": 35, "endTime": 37 }, + { "type": "Text", "name": "Jank 4", "startTime": 45, "endTime": 47 }, + { "type": "Text", "name": "Jank 5", "startTime": 55, "endTime": 57 }, + { "type": "Text", "name": "Jank 6", "startTime": 65, "endTime": 67 }, + { "type": "Text", "name": "Jank 7", "startTime": 75, "endTime": 77 }, + { "type": "Text", "name": "Jank 8", "startTime": 85, "endTime": 87 }, + { "type": "Text", "name": "Jank 9", "startTime": 95, "endTime": 97 }, + { + "type": "Text", + "name": "Jank 10", + "startTime": 105, + "endTime": 107 + }, + { + "type": "Text", + "name": "Jank 11", + "startTime": 115, + "endTime": 117 + }, + { + "type": "Text", + "name": "Jank 12", + "startTime": 125, + "endTime": 127 + }, + { + "type": "Text", + "name": "Jank 13", + "startTime": 135, + "endTime": 137 + }, + { + "type": "Text", + "name": "Jank 14", + "startTime": 145, + "endTime": 147 + }, + { + "type": "Text", + "name": "Jank 15", + "startTime": 155, + "endTime": 157 + }, + { + "type": "Text", + "name": "Jank 16", + "startTime": 165, + "endTime": 167 + }, + { + "type": "Text", + "name": "Jank 17", + "startTime": 175, + "endTime": 177 + }, + { + "type": "Text", + "name": "Jank 18", + "startTime": 185, + "endTime": 187 + }, + { + "type": "Text", + "name": "Jank 19", + "startTime": 195, + "endTime": 197 + }, + { + "type": "Text", + "name": "Jank 20", + "startTime": 205, + "endTime": 207 + }, + { + "type": "Text", + "name": "Jank 21", + "startTime": 215, + "endTime": 217 + }, + { + "type": "Text", + "name": "Jank 22", + "startTime": 225, + "endTime": 227 + }, + { + "type": "Text", + "name": "Jank 23", + "startTime": 235, + "endTime": 237 + }, + { + "type": "Text", + "name": "Jank 24", + "startTime": 245, + "endTime": 247 + } + ] + }, + "samples": { + "length": 7, + "stack": [1, 2, 2, 3, 0, 1, 4], + "time": [0, 1, 2, 3, 4, 5, 6], + "responsiveness": [0, 0, 0, 0, 0, 0, 0], + "rss": [null, null, null, null, null, null, null], + "uss": [null, null, null, null, null, null, null] + }, + "stringArray": [ + "(root)", + "0x100000f84", + "0x100001a45", + "Startup::XRE_Main", + "VsyncTimestamp", + "Reflow", + "baseline", + "frobnicate (chrome://blargh:34)", + "DOMEvent", + "MinorGC", + "Rasterize", + "firefox", + "chrome://blargh", + "frobnicate", + "Load 32: https://github.com/rustwasm/wasm-bindgen/issues/3", + "DiskIO", + "Load 1001: https://example.com/asset-01.js", + "Load 1002: https://example.com/asset-02.js", + "Load 1003: https://example.com/asset-03.js", + "Load 1004: https://example.com/asset-04.js", + "Load 1005: https://example.com/asset-05.js", + "Load 1006: https://example.com/asset-06.js", + "Load 1007: https://example.com/asset-07.js", + "Load 1008: https://example.com/asset-08.js", + "Load 1009: https://example.com/asset-09.js", + "Load 1010: https://example.com/asset-10.js", + "Load 1011: https://example.com/asset-11.js", + "Load 1012: https://example.com/asset-12.js", + "Load 1013: https://example.com/asset-13.js", + "Load 1014: https://example.com/asset-14.js", + "Load 1015: https://example.com/asset-15.js", + "Load 1016: https://example.com/asset-16.js", + "Load 1017: https://example.com/asset-17.js", + "Load 1018: https://example.com/asset-18.js", + "Load 1019: https://example.com/asset-19.js", + "Load 1020: https://example.com/asset-20.js", + "Load 1021: https://example.com/asset-21.js", + "Load 1022: https://example.com/asset-22.js", + "Load 1023: https://example.com/asset-23.js", + "Load 1024: https://example.com/asset-24.js", + "DocumentLoad", + "DOMContentLoaded", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank", + "Jank" + ] + }, + { + "name": "Compositor", + "processType": "default", + "pausedRanges": [], + "processStartupTime": 0, + "processShutdownTime": null, + "registerTime": 0, + "unregisterTime": null, + "libs": [ + { + "breakpadId": "F1D957D30B413D55A539BBA06F90DD8F0", + "arch": "x86_64", + "debugPath": "", + "debugName": "firefox", + "name": "firefox", + "path": "/Applications/FirefoxNightly.app/Contents/MacOS/firefox", + "start": 4294967296, + "end": 4294977296 + }, + { + "breakpadId": "1000000000000000000000000000000A1", + "arch": "x86_64", + "debugPath": "", + "debugName": "examplebinary", + "name": "examplebinary", + "path": "/tmp/examplebinary", + "start": 8589934592, + "end": 8589934612 + }, + { + "breakpadId": "100000000000000000000000000000A27", + "arch": "x86_64", + "debugPath": "", + "debugName": "examplebinary2.pdb", + "name": "examplebinary2", + "path": "C:\\examplebinary2", + "start": 8589934612, + "end": 8589934632 + } + ], + "frameTable": { + "length": 5, + "implementation": [null, null, null, null, 6], + "optimizations": [null, null, null, null, null], + "line": [null, null, null, 4391, 34], + "category": [null, null, null, 16, null], + "func": [0, 1, 2, 3, 4], + "address": [-1, 3972, 6725, -1, -1] + }, + "funcTable": { + "length": 5, + "name": [0, 1, 2, 3, 13], + "resource": [-1, 0, 0, -1, 1], + "address": [-1, 3972, 6725, -1, -1], + "isJS": [false, false, false, false, true], + "fileName": [null, null, null, null, 12], + "lineNumber": [null, null, null, null, 34] + }, + "resourceTable": { + "length": 2, + "type": [1, 5], + "name": [11, 12], + "lib": [0], + "icon": [], + "addonId": [], + "host": [] + }, + "stackTable": { + "length": 5, + "prefix": [null, 0, 1, 1, 1], + "frame": [0, 1, 2, 3, 4] + }, + "markers": { + "length": 7, + "name": [4, 5, 10, 10, 5, 8, 9], + "time": [0, 2, 4, 5, 8, 9, 11], + "data": [ + { "category": "VsyncTimestamp", "vsync": 0 }, + { + "category": "Paint", + "interval": "start", + "stack": { + "markers": { + "schema": { "name": 0, "time": 1, "data": 2 }, + "data": [] + }, + "name": "SyncProfile", + "samples": { + "schema": { + "stack": 0, + "time": 1, + "responsiveness": 2, + "rss": 3, + "uss": 4, + "frameNumber": 5, + "power": 6 + }, + "data": [[2, 1]] + } + }, + "type": "tracing" + }, + { "category": "Paint", "interval": "start", "type": "tracing" }, + { "category": "Paint", "interval": "end", "type": "tracing" }, + { "category": "Paint", "interval": "end", "type": "tracing" }, + { + "type": "DOMEvent", + "startTime": 9, + "endTime": 10, + "eventType": "mouseout", + "phase": 3 + }, + { "startTime": 11, "endTime": 12 } + ] + }, + "samples": { + "length": 7, + "stack": [1, 2, 2, 3, 0, 1, 4], + "time": [0, 1, 2, 3, 4, 5, 6], + "responsiveness": [0, 0, 0, 0, 0, 0, 0], + "rss": [null, null, null, null, null, null, null], + "uss": [null, null, null, null, null, null, null] + }, + "stringArray": [ + "(root)", + "0x100000f84", + "0x100001a45", + "Startup::XRE_Main", + "VsyncTimestamp", + "Reflow", + "baseline", + "frobnicate (chrome://blargh:34)", + "DOMEvent", + "MinorGC", + "Rasterize", + "firefox", + "chrome://blargh", + "frobnicate" + ] + }, + { + "name": "GeckoMain", + "processType": "tab", + "pausedRanges": [], + "processStartupTime": 1000, + "processShutdownTime": null, + "registerTime": 1000, + "unregisterTime": null, + "libs": [ + { + "breakpadId": "9F950E2CE3CD3E1ABD06D80788B606E60", + "arch": "x86_64", + "debugPath": "", + "debugName": "firefox-webcontent", + "name": "firefox-webcontent", + "path": "/Applications/FirefoxNightly.app/Contents/MacOS/firefox-webcontent.app/Contents/MacOS/firefox-webcontent", + "start": 4294967296, + "end": 4294977296 + }, + { + "breakpadId": "1000000000000000000000000000000A1", + "arch": "x86_64", + "debugPath": "", + "debugName": "examplebinary", + "name": "examplebinary", + "path": "/tmp/examplebinary", + "start": 8589934592, + "end": 8589934612 + }, + { + "breakpadId": "100000000000000000000000000000A27", + "arch": "x86_64", + "debugPath": "", + "debugName": "examplebinary2.pdb", + "name": "examplebinary2", + "path": "C:\\examplebinary2", + "start": 8589934612, + "end": 8589934632 + } + ], + "frameTable": { + "length": 7, + "implementation": [null, null, null, null, 6, null, null], + "optimizations": [null, null, null, null, null, null, null], + "line": [null, null, null, 4391, 34, null, null], + "category": [null, null, null, 16, null, 64, 16], + "func": [0, 1, 2, 3, 4, 5, 6], + "address": [-1, 3972, 6725, -1, -1, -1, -1] + }, + "funcTable": { + "length": 7, + "name": [0, 1, 2, 3, 13, 14, 15], + "resource": [-1, 0, 0, -1, 1, -1, -1], + "address": [-1, 3972, 6725, -1, -1, -1, -1], + "isJS": [false, false, false, false, true, false, false], + "fileName": [null, null, null, null, 12, null, null], + "lineNumber": [null, null, null, null, 34, null, null] + }, + "resourceTable": { + "length": 2, + "type": [1, 5], + "name": [11, 12], + "lib": [0, null], + "icon": [null, null], + "addonId": [null, null], + "host": [null, null] + }, + "stackTable": { + "length": 7, + "prefix": [null, 0, 1, 1, 2, 4, 5], + "frame": [0, 1, 2, 3, 5, 4, 6] + }, + "markers": { + "length": 8, + "name": [4, 5, 10, 10, 5, 8, 9, 11], + "time": [1000, 1002, 1004, 1005, 1008, 1009, 1011, 1013], + "data": [ + { "category": "VsyncTimestamp", "vsync": 0 }, + { + "category": "Paint", + "interval": "start", + "stack": { + "markers": { + "schema": { "name": 0, "time": 1, "data": 2 }, + "data": [] + }, + "name": "SyncProfile", + "samples": { + "schema": { + "stack": 0, + "time": 1, + "responsiveness": 2, + "rss": 3, + "uss": 4, + "frameNumber": 5, + "power": 6 + }, + "data": [[2, 1]] + } + }, + "type": "tracing" + }, + { "category": "Paint", "interval": "start", "type": "tracing" }, + { "category": "Paint", "interval": "end", "type": "tracing" }, + { "category": "Paint", "interval": "end", "type": "tracing" }, + { + "type": "DOMEvent", + "startTime": 1009, + "endTime": 1010, + "timeStamp": 1, + "eventType": "mouseout", + "phase": 3 + }, + { "startTime": 1011, "endTime": 1012 }, + { + "type": "Network", + "startTime": 1003, + "endTime": 1013, + "id": 388634410746504, + "status": "STATUS_STOP", + "pri": -20, + "count": 37838, + "URI": "https://github.com/rustwasm/wasm-bindgen/issues/4", + "domainLookupStart": 4, + "domainLookupEnd": 5, + "connectStart": 6, + "tcpConnectEnd": 7, + "secureConnectionStart": 8, + "connectEnd": 9, + "requestStart": 10, + "responseStart": 11, + "responseEnd": 12 + } + ] + }, + "samples": { + "length": 7, + "stack": [1, 2, 2, 3, 0, 1, 6], + "time": [1000, 1001, 1002, 1003, 1004, 1005, 1006], + "responsiveness": [0, 0, 0, 0, 0, 0, 0], + "rss": [null, null, null, null, null, null, null], + "uss": [null, null, null, null, null, null, null] + }, + "stringArray": [ + "(root)", + "0x100000f84", + "0x100001a45", + "Startup::XRE_Main", + "VsyncTimestamp", + "Reflow", + "baseline", + "frobnicate (chrome://blargh:34)", + "DOMEvent", + "MinorGC", + "Rasterize", + "firefox-webcontent", + "chrome://blargh", + "frobnicate", + "AutoEntryScript setTimeout handler", + "Element.getBoundingClientRect", + "Load 32: https://github.com/rustwasm/wasm-bindgen/issues/4" + ] + } + ], + "pages": [ + { + "tabID": 123, + "innerWindowID": 1, + "url": "https://example.com/", + "embedderInnerWindowID": 0 + } + ] +} diff --git a/profiler-cli/src/test/integration/basic.test.ts b/profiler-cli/src/test/integration/basic.test.ts index 598d729833..dbbd5ae9f3 100644 --- a/profiler-cli/src/test/integration/basic.test.ts +++ b/profiler-cli/src/test/integration/basic.test.ts @@ -21,6 +21,9 @@ import type { ProfileMetaResult, SessionMetadata, StatusResult, + ThreadMarkersResult, + ThreadNetworkResult, + ThreadPageLoadResult, ThreadSamplesResult, WithContext, } from '../../protocol'; @@ -323,6 +326,145 @@ describe('profiler-cli basic functionality', () => { expect(output).toContain('--max-lines must be a positive integer'); }); + it('--limit 0 means "no limit" on every command that takes --limit', async () => { + await cli(ctx, ['load', 'src/test/fixtures/upgrades/processed-1.json']); + + // `thread network` has always accepted 0; the others used to reject it. + // They must now all agree, otherwise `--limit 0` stays a coin flip. + for (const args of [ + ['thread', 'markers', '--list', '--limit', '0'], + ['thread', 'markers', '--limit', '0'], + ['thread', 'functions', '--limit', '0'], + ['thread', 'network', '--limit', '0'], + ['profile', 'logs', '--limit', '0'], + ['thread', 'page-load', '--jank-limit', '0'], + ]) { + const result = await cli(ctx, args); + const output = String(result.stdout || '') + String(result.stderr || ''); + expect({ args, exitCode: result.exitCode }).toEqual({ + args, + exitCode: 0, + }); + expect(output).not.toContain('must be a positive integer'); + } + + // "No limit" must actually mean everything, not an empty list: the fixture + // thread has 3 markers. + const all = await cli(ctx, [ + 'thread', + 'markers', + '--list', + '--limit', + '0', + '--json', + ]); + const allResult = JSON.parse( + all.stdout + ) as WithContext; + expect(allResult.flatMarkers).toHaveLength(allResult.filteredMarkerCount); + expect(allResult.filteredMarkerCount).toBeGreaterThan(1); + }); + + // The `--limit 0` tests above use a fixture with one network request and no + // jank, so they cannot tell "no limit" apart from "apply the default" — those + // only diverge once there is more data than the default shows. This fixture + // has 25 network requests and 24 jank periods on t-0, against defaults of 20 + // and 10 respectively, which is what makes the assertions below able to fail. + const LIMIT_FIXTURE = 'profiler-cli/src/test/fixtures/limit-boundaries.json'; + + it('network --limit 0 beats the built-in default of 20', async () => { + await cli(ctx, ['load', LIMIT_FIXTURE]); + + const asJson = async (args: string[]) => { + const result = await cli(ctx, args); + expect(result.exitCode).toBe(0); + return JSON.parse(result.stdout) as WithContext; + }; + + const base = ['thread', 'network', '--thread', 't-0', '--json']; + + const defaulted = await asJson(base); + expect(defaulted.filteredRequestCount).toBe(25); + expect(defaulted.requests).toHaveLength(20); + + const unlimited = await asJson([...base, '--limit', '0']); + expect(unlimited.requests).toHaveLength(25); + + const windowed = await asJson([...base, '--limit', '5']); + expect(windowed.requests).toHaveLength(5); + + // And the truncated default must point at the escape hatch. + const text = await cli(ctx, ['thread', 'network', '--thread', 't-0']); + expect(text.stdout).toContain('--limit 0'); + }); + + it('page-load --jank-limit 0 beats the built-in default of 10', async () => { + // This is the one sentinel that is genuinely load-bearing: `collectPageLoad` + // does `options.jankLimit ?? 10`, so forwarding an explicit 0 as "unset" + // silently reinstates the default. Without a fixture that has more than 10 + // jank periods, that regression is invisible. + await cli(ctx, ['load', LIMIT_FIXTURE]); + + const asJson = async (args: string[]) => { + const result = await cli(ctx, args); + expect(result.exitCode).toBe(0); + return JSON.parse(result.stdout) as WithContext; + }; + + const base = ['thread', 'page-load', '--thread', 't-0', '--json']; + + const defaulted = await asJson(base); + expect(defaulted.jankTotal).toBe(24); + expect(defaulted.jankPeriods).toHaveLength(10); + + const unlimited = await asJson([...base, '--jank-limit', '0']); + expect(unlimited.jankPeriods).toHaveLength(24); + + const windowed = await asJson([...base, '--jank-limit', '3']); + expect(windowed.jankPeriods).toHaveLength(3); + + // The truncated default must say so and name the escape hatch. + const text = await cli(ctx, ['thread', 'page-load', '--thread', 't-0']); + expect(text.stdout).toContain('Showing 10 of 24 jank periods'); + expect(text.stdout).toContain('--jank-limit 0'); + }); + + it('a negative or non-numeric --limit is still rejected', async () => { + await cli(ctx, ['load', 'src/test/fixtures/upgrades/processed-1.json']); + + for (const value of ['-1', 'abc']) { + const result = await cliFail(ctx, [ + 'thread', + 'markers', + '--list', + '--limit', + value, + ]); + expect(result.exitCode).not.toBe(0); + const output = String(result.stdout || '') + String(result.stderr || ''); + expect(output).toContain( + '--limit must be a non-negative integer (0 = no limit)' + ); + } + }); + + it('a truncated marker list says so and points at --limit 0', async () => { + await cli(ctx, ['load', 'src/test/fixtures/upgrades/processed-1.json']); + + const result = await cli(ctx, [ + 'thread', + 'markers', + '--list', + '--limit', + '1', + ]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('more markers omitted'); + expect(result.stdout).toContain('showing the first 1 of 3'); + expect(result.stdout).toContain('--limit 0'); + }); + it('build hash mismatch stops the daemon before cleaning up the session', async () => { const loadResult = await cli(ctx, [ 'load', diff --git a/profiler-cli/src/test/unit/limit-parsing.test.ts b/profiler-cli/src/test/unit/limit-parsing.test.ts new file mode 100644 index 0000000000..55c48b84c0 --- /dev/null +++ b/profiler-cli/src/test/unit/limit-parsing.test.ts @@ -0,0 +1,89 @@ +/* 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/. */ + +/** + * Unit tests for `--limit`-style argument parsing. + * + * These pin the sentinel contract itself, which is easy to get subtly wrong + * because two conventions are in play. The end-to-end consequences are covered + * in basic.test.ts against src/test/fixtures/limit-boundaries.json: + * + * - markers / functions / logs treat `undefined` as "no limit"; + * - network / page-load have a non-zero *default*, so for them "no limit" + * has to travel as `0` (an unset value means "apply the default"). + */ + +import { parseLimitArg } from '../../utils/parse'; + +describe('parseLimitArg', function () { + let exitSpy: jest.SpyInstance; + let errorSpy: jest.SpyInstance; + + beforeEach(function () { + // `process.exit` is typed as returning `never`; throwing keeps the control + // flow honest so a non-exiting parser cannot silently pass these tests. + exitSpy = jest.spyOn(process, 'exit').mockImplementation((code) => { + throw new Error(`process.exit(${code})`); + }) as unknown as jest.SpyInstance; + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(function () { + exitSpy.mockRestore(); + errorSpy.mockRestore(); + }); + + it('maps 0 to "no limit" rather than to a zero-length window', function () { + // The whole point of item B: 0 must not survive as the number 0, which + // every `slice(0, limit)` downstream would read as "show nothing". + expect(parseLimitArg('--limit', '0')).toBeUndefined(); + }); + + it('leaves an omitted value alone so defaults still apply', function () { + expect(parseLimitArg('--limit', undefined)).toBeUndefined(); + }); + + it('passes positive limits through unchanged', function () { + expect(parseLimitArg('--limit', '1')).toBe(1); + expect(parseLimitArg('--limit', '20')).toBe(20); + expect(parseLimitArg('--limit', '5000')).toBe(5000); + }); + + it('rejects negative and non-numeric values, naming the 0 case', function () { + for (const value of ['-1', '-100', 'abc', '']) { + expect(() => parseLimitArg('--limit', value)).toThrow( + /process\.exit\(1\)/ + ); + } + expect(errorSpy).toHaveBeenCalledWith( + 'Error: --limit must be a non-negative integer (0 = no limit)' + ); + }); + + it('names the flag it was given in the error message', function () { + expect(() => parseLimitArg('--jank-limit', '-1')).toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + 'Error: --jank-limit must be a non-negative integer (0 = no limit)' + ); + }); + + describe('the `?? 0` sentinel used by flags with a non-zero default', function () { + // `thread network --limit` and `thread page-load --jank-limit` keep their + // default in the CLI, so an explicit 0 has to be forwarded as something + // other than "unset". Only `jankLimit` is actually load-bearing: + // `collectPageLoad` does `jankLimit ?? 10`, so dropping its `?? 0` silently + // reinstates the default of 10. `collectThreadNetwork` reads 0 and + // `undefined` identically, so its `?? 0` is defensive. The end-to-end + // consequence of the load-bearing one is asserted in basic.test.ts. + it('turns an explicit 0 back into the 0 those queries expect', function () { + expect(parseLimitArg('--limit', '0') ?? 0).toBe(0); + expect(parseLimitArg('--jank-limit', '0') ?? 0).toBe(0); + }); + + it('does not disturb an explicit positive limit', function () { + expect(parseLimitArg('--limit', '5') ?? 0).toBe(5); + expect(parseLimitArg('--jank-limit', '3') ?? 0).toBe(3); + }); + }); +}); diff --git a/profiler-cli/src/test/unit/marker-formatting.test.ts b/profiler-cli/src/test/unit/marker-formatting.test.ts index 2356cef22a..7f4675fd27 100644 --- a/profiler-cli/src/test/unit/marker-formatting.test.ts +++ b/profiler-cli/src/test/unit/marker-formatting.test.ts @@ -137,6 +137,30 @@ describe('formatThreadMarkersResult flat list mode', function () { expect(output).toContain('✗'); }); + it('says how many markers were omitted and how to get them', function () { + const result = makeResult({ + totalMarkerCount: 100, + filteredMarkerCount: 7183, + flatMarkers: [makeFlat({ handle: 'm-1' }), makeFlat({ handle: 'm-2' })], + }); + + const output = formatThreadMarkersResult(result); + expect(output).toContain('7181 more markers omitted'); + expect(output).toContain('showing the first 2 of 7183'); + expect(output).toContain('--limit 0'); + }); + + it('does not claim truncation when the whole list is shown', function () { + const result = makeResult({ + filteredMarkerCount: 2, + flatMarkers: [makeFlat({ handle: 'm-1' }), makeFlat({ handle: 'm-2' })], + }); + + const output = formatThreadMarkersResult(result); + expect(output).not.toContain('omitted'); + expect(output).not.toContain('--limit 0'); + }); + it('does not show aggregated By Name header in flat list mode', function () { const result = makeResult({ filteredMarkerCount: 1, diff --git a/profiler-cli/src/utils/parse.ts b/profiler-cli/src/utils/parse.ts index 93a4c17260..1759e34869 100644 --- a/profiler-cli/src/utils/parse.ts +++ b/profiler-cli/src/utils/parse.ts @@ -32,6 +32,27 @@ export function parseFuncList(value: string): number[] { }); } +/** + * Parse a `--limit`-style flag. Returns `undefined` for `0` and for an omitted + * value, which downstream consumers treat as "no limit". + */ +export function parseLimitArg( + flagName: string, + value: string | undefined +): number | undefined { + if (value === undefined) { + return undefined; + } + const v = parseInt(value, 10); + if (isNaN(v) || v < 0) { + console.error( + `Error: ${flagName} must be a non-negative integer (0 = no limit)` + ); + process.exit(1); + } + return v === 0 ? undefined : v; +} + /** * Options bag produced by Commander for commands that support ephemeral sample filters. * Keys are camelCase because Commander normalises hyphenated option names.