browser_jsonview_profile_size.js (6250B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const TEST_JSON_URL = URL_ROOT + "simple_json.json"; 7 const PROFILER_URL_PREF = "devtools.performance.recording.ui-base-url"; 8 const TEST_PROFILER_URL = "http://127.0.0.1:8888"; 9 10 add_setup(async function () { 11 info("Setting profiler URL to localhost for tests"); 12 await SpecialPowers.pushPrefEnv({ 13 set: [ 14 [PROFILER_URL_PREF, TEST_PROFILER_URL], 15 ["devtools.jsonview.size-profiler.enabled", true], 16 ], 17 }); 18 }); 19 20 add_task(async function testProfileSizeButtonExists() { 21 info("Test that the Profile Size button exists in the tab bar"); 22 23 await addJsonViewTab(TEST_JSON_URL); 24 25 const buttonExists = await SpecialPowers.spawn( 26 gBrowser.selectedBrowser, 27 [], 28 () => { 29 const button = content.document.querySelector(".profiler-icon-button"); 30 return !!button; 31 } 32 ); 33 34 ok(buttonExists, "Profile Size button should exist in the tab bar"); 35 }); 36 37 add_task(async function testProfileSizePostMessage() { 38 info("Test that profile is sent via postMessage with correct handshake"); 39 40 await addJsonViewTab(TEST_JSON_URL); 41 42 const browser = gBrowser.selectedBrowser; 43 44 // Set up the mock for window.open before clicking 45 await SpecialPowers.spawn(browser, [TEST_PROFILER_URL], expectedUrl => { 46 const win = Cu.waiveXrays(content); 47 48 // Create test results object 49 win.testResults = { 50 windowUrl: null, 51 profile: null, 52 receivedReadyRequest: false, 53 messageOrigin: null, 54 resolved: false, 55 }; 56 57 // Mock window.open 58 win.open = Cu.exportFunction(function (url) { 59 win.testResults.windowUrl = url; 60 61 // Create mock window object with postMessage 62 const mockWindow = { 63 postMessage(message, origin) { 64 if (message.name === "ready:request") { 65 win.testResults.receivedReadyRequest = true; 66 win.testResults.messageOrigin = origin; 67 // Simulate profiler responding with ready:response 68 const event = new win.MessageEvent( 69 "message", 70 Cu.cloneInto( 71 { 72 origin: expectedUrl, 73 data: { name: "ready:response" }, 74 }, 75 win 76 ) 77 ); 78 win.dispatchEvent(event); 79 } else if (message.name === "inject-profile") { 80 win.testResults.profile = message.profile; 81 win.testResults.resolved = true; 82 } 83 }, 84 close() {}, 85 }; 86 87 return Cu.cloneInto(mockWindow, win, { cloneFunctions: true }); 88 }, win); 89 }); 90 91 // Click the button from within the content process 92 await SpecialPowers.spawn(browser, [], () => { 93 const button = content.document.querySelector(".profiler-icon-button"); 94 button.click(); 95 }); 96 97 // Wait for the test to complete 98 await TestUtils.waitForCondition( 99 async () => { 100 return SpecialPowers.spawn(browser, [], () => { 101 return Cu.waiveXrays(content).testResults?.resolved; 102 }); 103 }, 104 "Waiting for profile to be sent", 105 100, 106 100 107 ); 108 109 // Get the results 110 const result = await SpecialPowers.spawn(browser, [], () => { 111 return Cu.waiveXrays(content).testResults; 112 }); 113 114 ok(result.windowUrl, "window.open should have been called"); 115 ok( 116 result.windowUrl.includes("/from-post-message/"), 117 `URL should contain /from-post-message/, got: ${result.windowUrl}` 118 ); 119 ok( 120 result.windowUrl.includes(TEST_PROFILER_URL), 121 `URL should use preference URL ${TEST_PROFILER_URL}, got: ${result.windowUrl}` 122 ); 123 ok(result.receivedReadyRequest, "Should send ready:request"); 124 is( 125 result.messageOrigin, 126 TEST_PROFILER_URL, 127 "postMessage should use correct origin" 128 ); 129 ok(result.profile, "Should capture profile"); 130 ok(result.profile.meta, "Profile should have meta"); 131 ok(result.profile.threads, "Profile should have threads"); 132 }); 133 134 add_task(async function testProfileCreation() { 135 info("Test that a valid profile is created"); 136 137 const { createSizeProfile } = ChromeUtils.importESModule( 138 "resource://devtools/client/jsonview/json-size-profiler.mjs" 139 ); 140 141 const testJson = '{"name": "test", "value": 123}'; 142 const profile = createSizeProfile(testJson); 143 144 ok(profile.meta, "Profile should have meta object"); 145 ok(Array.isArray(profile.threads), "Profile should have threads array"); 146 ok( 147 Array.isArray(profile.meta.markerSchema), 148 "Profile meta should have markerSchema array" 149 ); 150 ok( 151 Array.isArray(profile.meta.categories), 152 "Profile meta should have categories array" 153 ); 154 Assert.greater( 155 profile.threads[0].samples.length, 156 0, 157 "Profile should have samples" 158 ); 159 160 // Validate total size of samples 161 const samples = profile.threads[0].samples; 162 const totalSize = samples.weight.reduce((sum, weight) => sum + weight, 0); 163 is( 164 totalSize, 165 testJson.length, 166 "Total sample size should match JSON string length" 167 ); 168 }); 169 170 add_task(async function testProfileCreationWithUtf8() { 171 info("Test that profile correctly handles UTF-8 multi-byte characters"); 172 173 const { createSizeProfile } = ChromeUtils.importESModule( 174 "resource://devtools/client/jsonview/json-size-profiler.mjs" 175 ); 176 177 // Test with various UTF-8 characters 178 // "café" - é is 2 bytes in UTF-8 179 // "中文" - each character is 3 bytes in UTF-8 180 // "🔥" - emoji is 4 bytes in UTF-8 181 const testJson = '{"name": "café", "lang": "中文", "emoji": "🔥"}'; 182 const profile = createSizeProfile(testJson); 183 184 // Calculate expected byte length (UTF-8 encoded) 185 const utf8Encoder = new TextEncoder(); 186 const expectedByteLength = utf8Encoder.encode(testJson).length; 187 188 const samples = profile.threads[0].samples; 189 const totalSize = samples.weight.reduce((sum, weight) => sum + weight, 0); 190 191 is( 192 totalSize, 193 expectedByteLength, 194 `Total sample size should match UTF-8 byte length (${expectedByteLength} bytes, not ${testJson.length} characters)` 195 ); 196 Assert.greater( 197 expectedByteLength, 198 testJson.length, 199 "UTF-8 byte length should be greater than character count for this test string" 200 ); 201 202 info(`Sample count: ${samples.length} for ${expectedByteLength} bytes`); 203 });