test_render_perf.html (7342B)
1 <!DOCTYPE HTML> 2 <html lang="en"> 3 <head> 4 <meta charset="utf8"> 5 <title>Test for getRepeatId()</title> 6 <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> 7 <script type="application/javascript" src="head.js"></script> 8 <!-- Any copyright is dedicated to the Public Domain. 9 - http://creativecommons.org/publicdomain/zero/1.0/ --> 10 </head> 11 <body> 12 <p>Test for render perf</p> 13 <div id="output"></div> 14 15 <script type="text/javascript"> 16 "use strict"; 17 18 // To analyze the profile results: 19 // > ./mach mochitest test_render_perf.html 20 // Then open https://profiler.firefox.com and drag the json file printed at the end of 21 // this test 22 23 const NUM_MESSAGES = 4000; 24 const NUM_STREAMING = 100; 25 const {FileUtils} = ChromeUtils.importESModule( 26 "resource://gre/modules/FileUtils.sys.mjs" 27 ); 28 Services.prefs.setIntPref("devtools.hud.loglimit", NUM_MESSAGES); 29 const WebConsoleWrapper = browserRequire("devtools/client/webconsole/webconsole-wrapper"); 30 const actions = 31 browserRequire("devtools/client/webconsole/actions/index"); 32 const EventEmitter = browserRequire("devtools/shared/event-emitter"); 33 const testPackets = Array.from({length: NUM_MESSAGES}).map((el, id) => ({ 34 "from": "server1.conn4.child1/consoleActor2", 35 "type": "consoleAPICall", 36 "message": { 37 "arguments": [ 38 "foobar", 39 `${id % 2 === 0 ? "Even" : "Odd"} text`, 40 id, 41 ], 42 "columnNumber": 1, 43 "counter": null, 44 "filename": "file:///test.html", 45 "functionName": "", 46 "groupName": "", 47 "level": "log", 48 "lineNumber": 1, 49 "private": false, 50 "styles": [], 51 "timeStamp": 1455064271115 + id, 52 "timer": null, 53 "workerType": "none", 54 "category": "webdev", 55 }, 56 })); 57 const lastPacket = testPackets.pop(); 58 59 function startMeasure(label) { 60 const startLabel = label + "start"; 61 performance.mark(startLabel); 62 return { 63 stop: (clear = true) => { 64 performance.measure(label, startLabel); 65 const entries = performance.getEntriesByName(label); 66 if (clear) { 67 performance.clearMeasures(label); 68 } 69 return entries[entries.length - 1]; 70 }, 71 }; 72 } 73 74 function wait(time) { 75 return new Promise(resolve => setTimeout(resolve, time)); 76 } 77 78 async function addAllMessages(wrapper) { 79 testPackets.forEach((packet) => wrapper.dispatchMessageAdd(packet)); 80 // Only wait for the last packet to minimize work. 81 await wrapper.dispatchMessageAdd(lastPacket, true); 82 await new Promise(resolve => requestAnimationFrame(resolve)); 83 } 84 85 async function addMessage(wrapper, message) { 86 await wrapper.dispatchMessageAdd(message, true); 87 } 88 89 function getTimes(times) { 90 times = times.sort(); 91 const totalTime = times.reduce((sum, t) => sum + t); 92 const avg = totalTime / times.length; 93 const median = times.length % 2 !== 0 94 ? times[Math.floor(times.length / 2)] 95 : (times[(times.length / 2) - 1] + times[times.length / 2]) / 2; 96 return {avg, median}; 97 } 98 99 async function clearMessages(wrapper) { 100 wrapper.dispatchMessagesClear(); 101 await new Promise(resolve => requestAnimationFrame(resolve)); 102 } 103 104 async function testBulkLogging(wrapper) { 105 await clearMessages(wrapper); 106 const bulkTimes = []; 107 const iterations = 5; 108 for (let i = 0; i < iterations; i++) { 109 const measure = startMeasure("bulk log"); 110 await addAllMessages(wrapper); 111 const {duration} = measure.stop(); 112 113 info(`took ${duration} ms to render bulk messages (iteration ${i})`); 114 bulkTimes.push(duration); 115 116 // Do not clear the store on last iteration so the next test can use the messages. 117 if (i !== iterations - 1) { 118 // Wait before clearing messages so those events are more spotable on the profile. 119 await wait(500); 120 await clearMessages(wrapper); 121 await wait(500); 122 } 123 } 124 const {avg, median} = getTimes(bulkTimes); 125 126 info(`BULK: On average, it took ${avg} ms (median ${median} ms) ` + 127 `to render ${NUM_MESSAGES} messages`); 128 } 129 130 async function testFiltering(wrapper) { 131 const measureFilter = startMeasure("filtering"); 132 133 const measureFilterOff = startMeasure("filtering off"); 134 await wrapper.getStore().dispatch(actions.filterToggle("log")); 135 const measureFilterOffEntry = measureFilterOff.stop(); 136 info(`Filter toggle time (off): ${measureFilterOffEntry.duration}`); 137 138 const measureFilterOn = startMeasure("filtering on"); 139 await wrapper.getStore().dispatch(actions.filterToggle("log")); 140 const measureFilterOnEntry = measureFilterOn.stop(); 141 info(`Filter toggle time (on): ${measureFilterOnEntry.duration}`); 142 measureFilter.stop(); 143 } 144 145 async function testStreamLogging(wrapper, clear = true) { 146 const streamMeasureLabel = "stream" + (clear === false ? " and prune" : ""); 147 const streamMeasure = startMeasure(streamMeasureLabel); 148 if (clear === true) { 149 await clearMessages(wrapper); 150 } 151 152 const measureLabel = "stream - add single message"; 153 for (let i = 0; i < NUM_STREAMING; i++) { 154 const measure = startMeasure(measureLabel); 155 await addMessage(wrapper, testPackets[i]); 156 measure.stop(false); 157 await new Promise(resolve => setTimeout(resolve, 100)); 158 } 159 160 const streamTimes = performance.getEntriesByName(measureLabel) 161 .map(entry => entry.duration); 162 performance.clearMeasures(measureLabel); 163 const { avg, median } = getTimes(streamTimes); 164 info(`STREAMING${clear === false ? " AND PRUNING" : ""}: On average, ` + 165 `it took ${avg} ms (median ${median} ms) for each message`); 166 streamMeasure.stop(); 167 } 168 169 window.onload = async function() { 170 // This test does costly work multiple times to have better performance data. 171 // It doesn't run in automation 172 SimpleTest.requestLongerTimeout(3); 173 174 const wrapper = new WebConsoleWrapper( 175 document.getElementById("output"), 176 {hud: EventEmitter.decorate({proxy: {}}), focus: () => {}}, 177 {}, 178 null, 179 document, 180 ); 181 wrapper.init(); 182 183 // From https://github.com/firefox-devtools/profiler/blob/b73eb73df04c7df51464fa50eeadef3dc7f5d4e2/docs/gecko-profile-format.md#L21 184 const settings = { 185 entries: 100000000, 186 interval: 1, 187 features: ["js"], 188 threads: ["GeckoMain"], 189 }; 190 Services.profiler.StartProfiler( 191 settings.entries, 192 settings.interval, 193 settings.features, 194 settings.threads, 195 ); 196 info("Profiler has started"); 197 198 await wait(500); 199 200 await testBulkLogging(wrapper); 201 202 await wait(500); 203 204 await testFiltering(wrapper); 205 206 await wait(500); 207 208 // first pass, without clearing the store. 209 await testStreamLogging(wrapper, false); 210 211 await wait(500); 212 // second pass, with an empty store. 213 await testStreamLogging(wrapper, true); 214 215 ok(true, "Tests finished"); 216 217 const file = new FileUtils.File(PathUtils.join(PathUtils.tempDir, `test_render_perf_${Date.now()}.json`)); 218 Services.profiler.dumpProfileToFile(file.path); 219 Services.profiler.StopProfiler(); 220 221 info(` 222 223 SAVING PROFILE: ${file.path} 224 225 To upload the profile, run the following commands: 226 227 gzip ${file.path} 228 curl -O https://raw.githubusercontent.com/firefox-devtools/profiler-server/master/tools/decode_jwt_payload.py 229 curl 'https://api.profiler.firefox.com/compressed-store' --compressed --data-binary @${file.path}.gz --header 'Accept: application/vnd.firefox-profiler+json;version=1' \\ 230 | python3 decode_jwt_payload.py \\ 231 | awk '{print "Hosted at: https://profiler.firefox.com/public/"$1}' 232 233 234 `); 235 236 SimpleTest.finish(); 237 }; 238 </script> 239 </body> 240 </html>