| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Binary vs String Transfer Benchmark</title> | |
| <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script> | |
| <style> | |
| body { | |
| font-family: Tahoma, Serif; | |
| font-size: 10pt; | |
| } | |
| .info { | |
| font-size: 12pt; | |
| } | |
| .left { | |
| text-align: left; | |
| } | |
| .right { | |
| text-align: right; | |
| } | |
| .positive { | |
| color: green; | |
| font-weight: bold; | |
| } | |
| .negative { | |
| color: red; | |
| font-weight: bold; | |
| } | |
| .center { | |
| text-align: center; | |
| } | |
| table.resultTable { | |
| border: 1px solid black; | |
| border-collapse: collapse; | |
| empty-cells: show; | |
| width: 100%; | |
| } | |
| table.resultTable td { | |
| padding: 2px 4px; | |
| border: 1px solid black; | |
| } | |
| table.resultTable > thead > tr { | |
| font-weight: bold; | |
| background: lightblue; | |
| } | |
| table.resultTable > tbody > tr:nth-child(odd) { | |
| background: white; | |
| } | |
| table.resultTable > tbody > tr:nth-child(even) { | |
| background: lightgray; | |
| } | |
| .hide { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body background-color="white"> | |
| <h1>Binary vs String Transfer Benchmark</h1> | |
| <table> | |
| <tr> | |
| <td> | |
| <p class="info"> | |
| This benchmark evaluates the message transfer speed between the | |
| renderer process and the browser process. <br />It compares the | |
| performance of binary and string message transfer. | |
| </p> | |
| <p class="info"> | |
| <b>Note:</b> There is no progress indication of the tests because it | |
| significantly influences measurements. <br />It usually takes 30 | |
| seconds (for 300 samples) to complete the tests. | |
| </p> | |
| </td> | |
| </tr> | |
| <tr> | |
| <td> | |
| Samples: | |
| <input | |
| id="sSamples" | |
| type="text" | |
| value="300" | |
| required | |
| pattern="[0-9]+" | |
| /> | |
| <button id="sRun" autofocus onclick="runTestSuite()">Run</button> | |
| </td> | |
| </tr> | |
| </table> | |
| <div style="padding-top: 10px; padding-bottom: 10px"> | |
| <table id="resultTable" class="resultTable"> | |
| <thead> | |
| <tr> | |
| <td class="center" style="width: 8%">Message Size</td> | |
| <td class="center" style="width: 8%"> | |
| String Round Trip Avg, ms | |
| </td> | |
| <td class="center" style="width: 8%"> | |
| Binary Round Trip Avg, ms | |
| </td> | |
| <td class="center" style="width: 10%">Relative Trip Difference</td> | |
| <td class="center" style="width: 8%">String Speed, MB/s</td> | |
| <td class="center" style="width: 8%">Binary Speed, MB/s</td> | |
| <td class="center" style="width: 10%">Relative Speed Difference</td> | |
| <td class="center" style="width: 8%">String Standard Deviation</td> | |
| <td class="center" style="width: 8%">Binary Standard Deviation</td> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <!-- result rows here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div id="round_trip_avg_chart"> | |
| <!-- Average round trip linear chart will be drawn inside this DIV --> | |
| </div> | |
| <div id="box_plot_chart"> | |
| <!-- Box plot of round trip time will be drawn inside this DIV --> | |
| </div> | |
| <script type="text/javascript"> | |
| let tests = []; | |
| let box_plot_test_data = []; | |
| let round_trip_avg_plot_data = []; | |
| function nextTestSuite(testIndex) { | |
| const nextTestIndex = testIndex + 1; | |
| setTimeout(execTestSuite, 0, nextTestIndex); | |
| } | |
| function generateRandomString(size) { | |
| // Symbols that will be encoded as two bytes in UTF-8 | |
| // so we compare transfer of the same amount of bytes | |
| const characters = | |
| "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя"; | |
| let randomString = ""; | |
| for (let i = 0; i < size; i++) { | |
| const randomIndex = Math.floor(Math.random() * characters.length); | |
| randomString += characters.charAt(randomIndex); | |
| } | |
| return randomString; | |
| } | |
| function generateRandomArrayBuffer(size) { | |
| const buffer = new ArrayBuffer(size); | |
| const uint8Array = new Uint8Array(buffer); | |
| for (let i = 0; i < uint8Array.length; i++) { | |
| uint8Array[i] = Math.floor(Math.random() * 256); | |
| } | |
| return buffer; | |
| } | |
| function reportError(errorCode, errorMessage) { | |
| console.error(`ErrorCode:${errorCode} Message:${errorMessage}`); | |
| } | |
| function sendString(size, testIndex) { | |
| const request = generateRandomString(size); | |
| const startTime = performance.now(); | |
| const onSuccess = (response) => { | |
| const roundTrip = performance.now() - startTime; | |
| const test = tests[testIndex]; | |
| test.totalRoundTrip += roundTrip; | |
| test.sample++; | |
| box_plot_test_data[testIndex].x.push(roundTrip); | |
| setTimeout(execTest, 0, testIndex); | |
| }; | |
| window.cefQuery({ | |
| request: request, | |
| onSuccess: onSuccess, | |
| onFailure: reportError, | |
| }); | |
| } | |
| function sendArrayBuffer(size, testIndex) { | |
| const request = generateRandomArrayBuffer(size); | |
| const startTime = performance.now(); | |
| const onSuccess = (response) => { | |
| const roundTrip = performance.now() - startTime; | |
| const test = tests[testIndex]; | |
| test.totalRoundTrip += roundTrip; | |
| test.sample++; | |
| box_plot_test_data[testIndex].x.push(roundTrip); | |
| setTimeout(execTest, 0, testIndex); | |
| }; | |
| window.cefQuery({ | |
| request: request, | |
| onSuccess: onSuccess, | |
| onFailure: reportError, | |
| }); | |
| } | |
| function getStandardDeviation(array, mean) { | |
| const n = array.length; | |
| if (n < 5) return null; | |
| return Math.sqrt( | |
| array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / | |
| (n - 1) | |
| ); | |
| } | |
| function execTest(testIndex) { | |
| const test = tests[testIndex]; | |
| if (test.sample >= test.totalSamples) { | |
| return nextTestSuite(testIndex); | |
| } | |
| test.func(test.length, test.index); | |
| } | |
| function column(prepared, value) { | |
| return ( | |
| "<td class='right'>" + (!prepared ? "-" : value.toFixed(3)) + "</td>" | |
| ); | |
| } | |
| function relativeDiffColumn(prepared, value, isBiggerBetter) { | |
| if (!prepared) return "<td class='right'>-</td>"; | |
| const isPositive = value >= 0 == isBiggerBetter; | |
| return [ | |
| "<td class='right ", | |
| isPositive ? "positive" : "negative", | |
| "'>", | |
| value > 0 ? "+" : "", | |
| value.toFixed(2), | |
| "%</td>", | |
| ].join(""); | |
| } | |
| function displayResult(test) { | |
| const id = "testResultRow_" + test.index; | |
| const markup = [ | |
| "<tr id='", | |
| id, | |
| "'>", | |
| "<td class='left'>", | |
| test.name, | |
| "</td>", | |
| column(test.prepared, test.avgRoundTrip), | |
| column(test.prepared, test.avgRoundTripBin), | |
| relativeDiffColumn(test.prepared, test.relativeTripDiff, false), | |
| column(test.prepared, test.speed), | |
| column(test.prepared, test.speedBinary), | |
| relativeDiffColumn(test.prepared, test.relativeSpeedDiff, true), | |
| "<td class='right'>", | |
| !test.prepared || test.stdDeviation == null | |
| ? "-" | |
| : test.stdDeviation.toFixed(3), | |
| "</td>", | |
| "<td class='right'>", | |
| !test.prepared || test.stdDeviationBinary == null | |
| ? "-" | |
| : test.stdDeviationBinary.toFixed(3), | |
| "</td>", | |
| "</tr>", | |
| ].join(""); | |
| const row = document.getElementById(id); | |
| if (row) { | |
| row.outerHTML = markup; | |
| } else { | |
| const tbody = document.getElementById("resultTable").tBodies[0]; | |
| tbody.insertAdjacentHTML("beforeEnd", markup); | |
| } | |
| } | |
| function relativeDiff(left, right) { | |
| if (right != 0) { | |
| return ((left - right) / right) * 100; | |
| } | |
| return 0; | |
| } | |
| function buildTestResults(tests) { | |
| testResults = []; | |
| let oldRoundTrip = { | |
| x: [], | |
| y: [], | |
| type: "scatter", | |
| name: "String", | |
| }; | |
| let newRoundTrip = { | |
| x: [], | |
| y: [], | |
| type: "scatter", | |
| name: "Binary", | |
| }; | |
| for (let i = 0; i < tests.length / 2; i++) { | |
| const index = testResults.length; | |
| // Tests are in pairs - String and Binary | |
| const test = tests[i * 2]; | |
| const testBin = tests[i * 2 + 1]; | |
| const avgRoundTrip = test.totalRoundTrip / test.totalSamples; | |
| const avgRoundTripBin = testBin.totalRoundTrip / testBin.totalSamples; | |
| const relativeTripDiff = relativeDiff(avgRoundTripBin, avgRoundTrip); | |
| // In MB/s | |
| const speed = test.byteSize / (avgRoundTrip * 1000); | |
| const speedBinary = testBin.byteSize / (avgRoundTripBin * 1000); | |
| const relativeSpeedDiff = relativeDiff(speedBinary, speed); | |
| const stdDeviation = getStandardDeviation( | |
| box_plot_test_data[test.index].x, | |
| avgRoundTrip | |
| ); | |
| const stdDeviationBinary = getStandardDeviation( | |
| box_plot_test_data[testBin.index].x, | |
| avgRoundTripBin | |
| ); | |
| testResults.push({ | |
| name: humanFileSize(test.byteSize), | |
| index: index, | |
| prepared: true, | |
| avgRoundTrip: avgRoundTrip, | |
| avgRoundTripBin: avgRoundTripBin, | |
| relativeTripDiff: relativeTripDiff, | |
| speed: speed, | |
| speedBinary: speedBinary, | |
| relativeSpeedDiff: relativeSpeedDiff, | |
| stdDeviation: stdDeviation, | |
| stdDeviationBinary: stdDeviationBinary, | |
| }); | |
| oldRoundTrip.x.push(test.byteSize); | |
| newRoundTrip.x.push(test.byteSize); | |
| oldRoundTrip.y.push(avgRoundTrip); | |
| newRoundTrip.y.push(avgRoundTripBin); | |
| } | |
| round_trip_avg_plot_data = [oldRoundTrip, newRoundTrip]; | |
| return testResults; | |
| } | |
| function buildEmptyTestResults(tests) { | |
| testResults = []; | |
| for (let i = 0; i < tests.length / 2; i++) { | |
| const index = testResults.length; | |
| const test = tests[i * 2]; | |
| testResults.push({ | |
| name: humanFileSize(test.byteSize), | |
| index: index, | |
| prepared: false, | |
| }); | |
| } | |
| return testResults; | |
| } | |
| function resetTestsResults(totalSamples) { | |
| if (totalSamples <= 0) totalSamples = 1; | |
| // Reset tests results | |
| tests.forEach((test) => { | |
| test.sample = 0; | |
| test.totalRoundTrip = 0; | |
| test.totalSamples = totalSamples; | |
| }); | |
| testResults = buildEmptyTestResults(tests); | |
| testResults.forEach((result) => displayResult(result)); | |
| round_trip_avg_plot_data = []; | |
| box_plot_test_data.forEach((data) => { | |
| data.x = []; | |
| }); | |
| } | |
| function queueTest(name, byteSize, length, testFunc) { | |
| const testIndex = tests.length; | |
| test = { | |
| name: name, | |
| byteSize: byteSize, | |
| length: length, | |
| index: testIndex, | |
| sample: 0, | |
| totalRoundTrip: 0, | |
| func: testFunc, | |
| }; | |
| tests.push(test); | |
| box_plot_test_data.push({ | |
| x: [], | |
| type: "box", | |
| boxpoints: "all", | |
| name: name, | |
| jitter: 0.3, | |
| pointpos: -1.8, | |
| }); | |
| } | |
| function execTestSuite(testIndex) { | |
| if (testIndex < tests.length) { | |
| execTest(testIndex); | |
| } else { | |
| testsRunFinished(); | |
| } | |
| } | |
| function startTests() { | |
| // Let the updated table render before starting the tests | |
| setTimeout(execTestSuite, 200, 0); | |
| } | |
| function execQueuedTests(totalSamples) { | |
| resetTestsResults(totalSamples); | |
| startTests(); | |
| } | |
| function setSettingsState(disabled) { | |
| document.getElementById("sSamples").disabled = disabled; | |
| document.getElementById("sRun").disabled = disabled; | |
| } | |
| function testsRunFinished() { | |
| testResults = buildTestResults(tests); | |
| testResults.forEach((result) => displayResult(result)); | |
| const round_trip_layout = { | |
| title: "Average round trip, μs (Smaller Better)", | |
| }; | |
| Plotly.newPlot( | |
| "round_trip_avg_chart", | |
| round_trip_avg_plot_data, | |
| round_trip_layout | |
| ); | |
| const box_plot_layout = { | |
| title: "Round Trip Time, μs", | |
| }; | |
| Plotly.newPlot("box_plot_chart", box_plot_test_data, box_plot_layout); | |
| setSettingsState(false); | |
| } | |
| function humanFileSize(bytes) { | |
| const step = 1024; | |
| const originalBytes = bytes; | |
| if (Math.abs(bytes) < step) { | |
| return bytes + " B"; | |
| } | |
| const units = [" KB", " MB", " GB"]; | |
| let u = -1; | |
| let count = 0; | |
| do { | |
| bytes /= step; | |
| u += 1; | |
| count += 1; | |
| } while (Math.abs(bytes) >= step && u < units.length - 1); | |
| return bytes.toString() + units[u]; | |
| } | |
| window.runTestSuite = () => { | |
| Plotly.purge("round_trip_avg_chart"); | |
| Plotly.purge("box_plot_chart"); | |
| setSettingsState(true); | |
| const totalSamples = parseInt( | |
| document.getElementById("sSamples").value | |
| ); | |
| execQueuedTests(totalSamples); | |
| }; | |
| const totalSamples = parseInt(document.getElementById("sSamples").value); | |
| queueTest("Empty String", 0, 0, sendString); | |
| queueTest("Empty Binary", 0, 0, sendArrayBuffer); | |
| for (let byteSize = 8; byteSize <= 512 * 1024; byteSize *= 2) { | |
| // Byte size of a string is twice its length because of UTF-16 encoding | |
| const stringLen = byteSize / 2; | |
| queueTest( | |
| humanFileSize(byteSize) + " String", | |
| byteSize, | |
| stringLen, | |
| sendString | |
| ); | |
| queueTest( | |
| humanFileSize(byteSize) + " Binary", | |
| byteSize, | |
| byteSize, | |
| sendArrayBuffer | |
| ); | |
| } | |
| resetTestsResults(totalSamples); | |
| </script> | |
| </body> | |
| </html> |