Recently Written · git

bd-plugin-profiler

BetterDiscord plugin that measures the main-thread time of each plugin

git clone https://github.com/equwal/bd-plugin-profiler

Log | Files | Refs


test/bd-patcher.test.js (4822 bytes)

1 "use strict";
2 const test = require("node:test");
3 const assert = require("node:assert/strict");
4 const {loadBdPatcher} = require("./helpers/installed.js");
5 const {Recorder, wrapPatchChild, unwrapPatchChild} = require("../00PluginProfiler.plugin.js").internals;
6 
7 function setup() {
8     const errors = [];
9     const Patcher = loadBdPatcher({err: (...args) => errors.push(args)});
10     const clock = {t: 0};
11     const recorder = new Recorder(() => clock.t);
12     const msByOwner = () => Object.fromEntries([...recorder.owners].map(([name, entry]) => [name, entry.ms]));
13     const wrapAll = callers => {
14         const children = callers.flatMap(caller => Patcher.getPatchesByCaller(caller));
15         for (const child of children) assert.equal(wrapPatchChild(recorder, child, child.caller), true);
16         return children;
17     };
18     return {Patcher, errors, clock, recorder, msByOwner, wrapAll};
19 }
20 
21 test("patch time goes to the plugin, the original function time does not, and results stay the same", () => {
22     const {Patcher, errors, clock, recorder, msByOwner, wrapAll} = setup();
23     const target = {work(x) { clock.t += 100; return x * 2; }};
24     Patcher.before("A", target, "work", () => { clock.t += 5; });
25     Patcher.instead("B", target, "work", (self, args, next) => { clock.t += 7; return next(...args) + 1; });
26     Patcher.after("C", target, "work", (self, args, value) => { clock.t += 3; return value + 10; });
27     const expected = target.work(4);
28     assert.equal(expected, 19);
29 
30     const children = wrapAll(["A", "B", "C"]);
31     assert.equal(target.work(4), expected);
32     assert.deepEqual(msByOwner(), {A: 5, B: 7, C: 3});
33     assert.equal(recorder.unattributedMs, 100);
34 
35     // A second scan must not wrap a wrapper.
36     for (const child of children) assert.equal(wrapPatchChild(recorder, child, child.caller), false);
37     for (const child of children) unwrapPatchChild(child);
38     assert.equal(target.work(4), expected);
39     assert.deepEqual(errors, []);
40 });
41 
42 test("unwrap puts back the exact callbacks", () => {
43     const {Patcher, wrapAll} = setup();
44     const target = {work() { return 1; }};
45     const before = () => {};
46     const after = (self, args, value) => value;
47     Patcher.before("A", target, "work", before);
48     Patcher.after("A", target, "work", after);
49     const children = wrapAll(["A"]);
50     assert.notEqual(children[0].callback, before);
51     for (const child of children) unwrapPatchChild(child);
52     assert.deepEqual(children.map(child => child.callback), [before, after]);
53 });
54 
55 test("chained instead patches each get only their own time", () => {
56     const {Patcher, clock, recorder, msByOwner, wrapAll} = setup();
57     const target = {work(x) { clock.t += 40; return x; }};
58     Patcher.instead("B1", target, "work", (self, args, next) => { clock.t += 2; const value = next(...args); clock.t += 1; return value + 1; });
59     Patcher.instead("B2", target, "work", (self, args, next) => { clock.t += 4; return next(...args) * 10; });
60     const expected = target.work(1);
61     wrapAll(["B1", "B2"]);
62     assert.equal(target.work(1), expected);
63     assert.deepEqual(msByOwner(), {B1: 3, B2: 4});
64     assert.equal(recorder.unattributedMs, 40);
65 });
66 
67 test("a patch that calls a patched function pays for the Discord work but not for the other plugin", () => {
68     const {Patcher, clock, msByOwner, wrapAll} = setup();
69     const inner = {step() { clock.t += 10; }};
70     const outer = {work() { clock.t += 1; }};
71     Patcher.after("C", inner, "step", () => { clock.t += 6; });
72     Patcher.before("A", outer, "work", () => { clock.t += 2; inner.step(); });
73     wrapAll(["A", "C"]);
74     outer.work();
75     // Without plugin A, Discord does not call inner.step here. So A owns those 10 ms. Plugin C owns its 6 ms.
76     assert.deepEqual(msByOwner(), {A: 12, C: 6});
77 });
78 
79 test("a plugin can unpatch while the profiler wraps its callback", () => {
80     const {Patcher, clock, msByOwner, wrapAll} = setup();
81     const target = {work() { return "original"; }};
82     const unpatch = Patcher.after("A", target, "work", () => { clock.t += 1; return "patched"; });
83     wrapAll(["A"]);
84     assert.equal(target.work(), "patched");
85     unpatch();
86     assert.equal(target.work(), "original");
87     assert.deepEqual(Patcher.getPatchesByCaller("A"), []);
88     assert.deepEqual(msByOwner(), {A: 1});
89 });
90 
91 test("an error in a wrapped callback reaches the BetterDiscord logger as before", () => {
92     const {Patcher, errors, recorder, wrapAll} = setup();
93     const target = {work() { return 5; }};
94     Patcher.before("A", target, "work", () => { throw new Error("plugin bug"); });
95     wrapAll(["A"]);
96     assert.equal(target.work(), 5);
97     assert.equal(errors.length, 1);
98     assert.equal(errors[0][2].message, "plugin bug");
99     assert.equal(recorder.stack.length, 0);
100     assert.equal(recorder.owners.get("A").calls, 1);
101 });