From 24760e51db1ec403638a53b0b3862ea4bf71d429 Mon Sep 17 00:00:00 2001 From: Marks Polakovs Date: Tue, 26 May 2020 14:54:13 +0200 Subject: [PATCH] Add semi-automated script for tracing memory leak --- package.json | 3 +- scripts/.gitignore | 1 + scripts/trace-memory-leak.ts | 128 +++++++++++++++++++++++++++++++++++ src/showplanner/Item.tsx | 1 + yarn.lock | 7 ++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 scripts/.gitignore create mode 100644 scripts/trace-memory-leak.ts diff --git a/package.json b/package.json index 3ebe2a7..e12c7ce 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "workbox-webpack-plugin": "^5.1.2", "worklet-loader": "^1.0.0" }, - "homepage": "https://ury.org.uk/webstudio", + "homepage": "https://ury.org.uk", "scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js", @@ -128,6 +128,7 @@ ] }, "devDependencies": { + "@types/puppeteer": "^3.0.0", "@ury1350/prettier-config": "^1.0.0", "node-sass": "^4.13.1", "prettier": "^1.19.1" diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..bd59350 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +.credentials diff --git a/scripts/trace-memory-leak.ts b/scripts/trace-memory-leak.ts new file mode 100644 index 0000000..cf7b598 --- /dev/null +++ b/scripts/trace-memory-leak.ts @@ -0,0 +1,128 @@ +import * as fs from "fs"; +import * as path from "path"; + +/// +const puppeteer = require("puppeteer"); + +/* + * Memory Leak Tracer 2020 + * written by Marks Polakovs, send all hate mail to marks.polakovs@ury.org.uk + * + * Requirements: + * puppeteer and ts-node installed globally + * You will also need the NODE_PATH environment variable set to wherever your global node_modules is + * To find it, run `yarn global bin` and replace /bin with /node_modules + * (e.g. C:\Users\Marks\scoop\apps\yarn\current\global\node_modules) + * + * Usage: + * Run this script as `ts-node --skip-project scripts/trace-memory-leak.ts http://local-development.ury.org.uk` + * substituting the URL for whatever URL you want to test. Don't include any query parameters, + * the script will take care of it. + * + * It may redirect you to a MyRadio sign in page. Don't worry, it won't steal your password (yet). + * + * To skip login, put your MyRadio username and password into a file called ".credentials" like + * `username:password` + * + * Once running, just wait until it prints out the status. In addition, if it detects the leak it will exit + * with a code of 1 - useful in "git bisect" for example. + * If something goes wrong that isn't a memory leak, it'll exit with an exit code over 128. + */ + +const baseUrl = process.argv[2]; +if (typeof baseUrl !== "string") { + console.log("Expected a baseURL as the first and only parameter!"); + process.exit(129); +} + +// Specially doctored timeslot, do not change unless you know what you are doing! +const TIMESLOT_ID = 147595; + +(async () => { + try { + console.log("Setting up..."); + const browser = await puppeteer.launch({ + headless: false, + defaultViewport: { width: 1366, height: 768 }, + args: [`--window-size=1440,960`] + }); + const page = await browser.newPage(); + await page.goto(baseUrl + "?timeslot_id=" + TIMESLOT_ID.toString(10)); + await page.waitForNavigation({'waitUntil': 'networkidle0'}); + if (page.url().indexOf("login") > -1) { + if (fs.existsSync(path.join(__dirname, ".credentials"))) { + const [username, password] = fs.readFileSync(path.join(__dirname, ".credentials"), { encoding: "utf-8" }).split(":"); + await page.type("#myradio_login-user", username); + await page.type("#myradio_login-password", password); + if ((await page.$("#myradio_login-submit")) !== null) { + await page.click("#myradio_login-submit"); + } + try { + await page.waitForSelector("#signin-submit", {visible: true, timeout: 10000}); + await page.click("#signin-submit"); + } catch (e) { + console.warn(e) + console.warn("Signing in went a bit wrong, please do it manually. Thank!"); + } + } else { + console.log("Please sign in in the browser window. Thank! (Choose whatever timeslot you like, it'll get ignored.)"); + } + } + + await page.waitForSelector(".ReactModal__Content", { visible: true }); + await page.click(".ReactModal__Content button.btn-primary"); + + console.log("Starting test: loading songs 1-3"); + await Promise.all([0,1,2].map(id => page.click(`#channel-${id} div.item:nth-child(1)`))); + + await Promise.all([0,1,2].map(id => page.waitForSelector(`#channel-${id} wave canvas`, { visible: true }))); + + console.log("Songs loaded; waiting five seconds for memory usage to stabilise..."); + await page.waitFor(5000); + const stats1 = await page.metrics(); + console.log(`JS Heap total at time ${stats1.Timestamp}: ${stats1.JSHeapTotalSize}, used ${stats1.JSHeapTotalSize}`); + + const arrayBufferHandle = await page.evaluateHandle(() => ArrayBuffer.prototype); + const buffers1 = await page.queryObjects(arrayBufferHandle); + const buffersCount1 = await page.evaluate(bufs => bufs.length, buffers1); + console.log(`ArrayBuffers found: ${buffersCount1}`); + await buffers1.dispose(); + + console.log("Loading songs 4-6...") + + await Promise.all([0,1,2].map(id => page.click(`#channel-${id} div.item:nth-child(2)`))); + await page.waitFor(1000); + await Promise.all([0,1,2].map(id => page.waitForSelector(`#channel-${id} wave canvas`, { visible: true }))); + + console.log("Songs loaded; waiting five seconds for memory usage to stabilise..."); + await page.waitFor(5000); + + const stats2 = await page.metrics(); + console.log(`JS Heap total at time ${stats2.Timestamp}: ${stats2.JSHeapTotalSize}, used ${stats2.JSHeapTotalSize}`); + + const buffers2 = await page.queryObjects(arrayBufferHandle); + const buffersCount2 = await page.evaluate(bufs => bufs.length, buffers2); + console.log(`ArrayBuffers found: ${buffersCount2}`); + await buffers2.dispose(); + + console.log("\r\n\r\n"); + + const leakThresholdHeap = stats1.JSHeapUsedSize * 1.5; + const leakThresholdBuffers = buffersCount1 * 1.5; + + console.log(`Leak threshold: heap ${leakThresholdHeap}, buffers ${leakThresholdBuffers}`) + const leakDetected = (stats2.JSHeapUsedSize > leakThresholdHeap) || ((buffersCount2 * 1.0) > leakThresholdBuffers); + console.log(leakDetected ? "\r\n\r\nLeak detected!\r\n\r\n" : "\r\n\r\nLeak not detected!\r\n\r\n"); + + console.log("Cleaning up..."); + await arrayBufferHandle.dispose(); + await page.close(); + await browser.close(); + console.log("Done!"); + process.exit(leakDetected ? 1 : 0); + } catch (e) { + console.error("Something exploded!"); + console.error(e); + process.exit(130); + } +})(); diff --git a/src/showplanner/Item.tsx b/src/showplanner/Item.tsx index ffbe240..f358418 100644 --- a/src/showplanner/Item.tsx +++ b/src/showplanner/Item.tsx @@ -54,6 +54,7 @@ export const Item = memo(function Item({
= 0 && playerState && diff --git a/yarn.lock b/yarn.lock index 044dc4d..fc05223 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1575,6 +1575,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/puppeteer@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-3.0.0.tgz#24cdcc131e319477608d893f0017e08befd70423" + integrity sha512-59+fkfHHXHzX5rgoXIMnZyzum7ZLx/Wc3fhsOduFThpTpKbzzdBHMZsrkKGLunimB4Ds/tI5lXTRLALK8Mmnhg== + dependencies: + "@types/node" "*" + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"