mirror of
https://github.com/Instadapp/Swap-Aggregator-Subgraph.git
synced 2024-07-29 21:57:12 +00:00
347 lines
10 KiB
JavaScript
347 lines
10 KiB
JavaScript
// WebAssembly pages are 65536 kb
|
|
const PAGE_SIZE_BITS = 16;
|
|
const PAGE_SIZE = 1 << PAGE_SIZE_BITS;
|
|
const PAGE_MASK = PAGE_SIZE - 1;
|
|
|
|
// Wasm32 pointer size is 4 bytes
|
|
const PTR_SIZE_BITS = 2;
|
|
const PTR_SIZE = 1 << PTR_SIZE_BITS;
|
|
const PTR_MASK = PTR_SIZE - 1;
|
|
const PTR_VIEW = Uint32Array;
|
|
|
|
export const BLOCK_OVERHEAD = PTR_SIZE;
|
|
export const OBJECT_OVERHEAD = 16;
|
|
export const TOTAL_OVERHEAD = BLOCK_OVERHEAD + OBJECT_OVERHEAD;
|
|
|
|
function assert(x) {
|
|
if (!x) throw Error("assertion failed");
|
|
return x;
|
|
}
|
|
|
|
Error.stackTraceLimit = 15;
|
|
|
|
function trimStacktrace(stack, levels) {
|
|
return stack.split(/\r?\n/).slice(1 + levels);
|
|
}
|
|
|
|
const hrtime = typeof performance !== "undefined" && performance.now
|
|
? performance.now
|
|
: typeof process !== "undefined" && process.hrtime
|
|
? () => { let t = process.hrtime(); return t[0] * 1e3 + t[1] / 1e6; }
|
|
: Date.now;
|
|
|
|
const mmTagsToString = [
|
|
"",
|
|
"FREE",
|
|
"LEFTFREE",
|
|
"FREE+LEFTFREE"
|
|
];
|
|
|
|
const gcColorToString = [
|
|
"BLACK/WHITE",
|
|
"WHITE/BLACK",
|
|
"GRAY",
|
|
"INVALID"
|
|
];
|
|
|
|
export class Rtrace {
|
|
|
|
constructor(options) {
|
|
this.options = options || {};
|
|
this.onerror = this.options.onerror || function() { /* nop */ };
|
|
this.oninfo = this.options.oninfo || function() { /* nop */ };
|
|
this.oncollect_ = this.options.oncollect || function() { /* nop */ };
|
|
this.memory = null;
|
|
this.shadow = null;
|
|
this.shadowStart = 0x100000000;
|
|
this.blocks = new Map();
|
|
this.allocSites = new Map();
|
|
this.freedBlocks = new Map();
|
|
this.gcProfileStart = 0;
|
|
this.gcProfile = [];
|
|
|
|
this.allocCount = 0;
|
|
this.resizeCount = 0;
|
|
this.moveCount = 0;
|
|
this.freeCount = 0;
|
|
this.heapBase = 0x100000000;
|
|
}
|
|
|
|
install(imports) {
|
|
if (!imports) imports = {};
|
|
imports.rtrace = Object.assign(imports.rtrace || {}, {
|
|
oninit: this.oninit.bind(this),
|
|
onalloc: this.onalloc.bind(this),
|
|
onresize: this.onresize.bind(this),
|
|
onmove: this.onmove.bind(this),
|
|
onvisit: this.onvisit.bind(this),
|
|
onfree: this.onfree.bind(this),
|
|
oninterrupt: this.oninterrupt.bind(this),
|
|
onyield: this.onyield.bind(this),
|
|
oncollect: this.oncollect.bind(this),
|
|
onstore: this.onstore.bind(this),
|
|
onload: this.onload.bind(this)
|
|
});
|
|
return imports;
|
|
}
|
|
|
|
/** Synchronizes the shadow memory with the module's memory. */
|
|
syncShadow() {
|
|
if (!this.memory) {
|
|
this.memory = assert(this.options.getMemory());
|
|
this.shadow = new WebAssembly.Memory({
|
|
initial: ((this.memory.buffer.byteLength + PAGE_MASK) & ~PAGE_MASK) >>> PAGE_SIZE_BITS
|
|
});
|
|
} else {
|
|
var diff = this.memory.buffer.byteLength - this.shadow.buffer.byteLength;
|
|
if (diff > 0) this.shadow.grow(diff >>> 16);
|
|
}
|
|
}
|
|
|
|
/** Marks a block's presence in shadow memory. */
|
|
markShadow(info, oldSize = 0) {
|
|
assert(this.shadow && this.shadow.byteLength == this.memory.byteLength);
|
|
assert((info.size & PTR_MASK) == 0);
|
|
if (info.ptr < this.shadowStart) {
|
|
this.shadowStart = info.ptr;
|
|
}
|
|
var len = info.size >>> PTR_SIZE_BITS;
|
|
var view = new PTR_VIEW(this.shadow.buffer, info.ptr, len);
|
|
var errored = false;
|
|
var start = oldSize >>> PTR_SIZE_BITS;
|
|
for (let i = 0; i < start; ++i) {
|
|
if (view[i] != info.ptr && !errored) {
|
|
this.onerror(Error("shadow region mismatch: " + view[i] + " != " + info.ptr), info);
|
|
errored = true;
|
|
}
|
|
}
|
|
errored = false;
|
|
for (let i = start; i < len; ++i) {
|
|
if (view[i] != 0 && !errored) {
|
|
this.onerror(Error("shadow region already in use: " + view[i] + " != 0"), info);
|
|
errored = true;
|
|
}
|
|
view[i] = info.ptr;
|
|
}
|
|
}
|
|
|
|
/** Unmarks a block's presence in shadow memory. */
|
|
unmarkShadow(info, oldSize = info.size) {
|
|
assert(this.shadow && this.shadow.byteLength == this.memory.byteLength);
|
|
var len = oldSize >>> PTR_SIZE_BITS;
|
|
var view = new PTR_VIEW(this.shadow.buffer, info.ptr, len);
|
|
var errored = false;
|
|
var start = 0;
|
|
if (oldSize != info.size) {
|
|
assert(oldSize > info.size);
|
|
start = info.size >>> PTR_SIZE_BITS;
|
|
}
|
|
for (let i = 0; i < len; ++i) {
|
|
if (view[i] != info.ptr && !errored) {
|
|
this.onerror(Error("shadow region mismatch: " + view[i] + " != " + info.ptr), info);
|
|
errored = true;
|
|
}
|
|
if (i >= start) view[i] = 0;
|
|
}
|
|
}
|
|
|
|
/** Performs an access to shadow memory. */
|
|
accessShadow(ptr, size, isLoad, isRT) {
|
|
this.syncShadow();
|
|
if (ptr < this.shadowStart) return;
|
|
var value = new Uint32Array(this.shadow.buffer, ptr & ~PTR_MASK, 1)[0];
|
|
if (value != 0) return;
|
|
if (!isRT) {
|
|
let stack = trimStacktrace(new Error().stack, 2);
|
|
this.onerror(new Error("OOB " + (isLoad ? "load" : "store") + (8 * size) + " at address " + ptr + "\n" + stack.join("\n")));
|
|
}
|
|
}
|
|
|
|
/** Obtains information about a block. */
|
|
getBlockInfo(ptr) {
|
|
const [
|
|
mmInfo,
|
|
gcInfo,
|
|
gcInfo2,
|
|
rtId,
|
|
rtSize
|
|
] = new Uint32Array(this.memory.buffer, ptr, 5);
|
|
const size = mmInfo & ~3;
|
|
return {
|
|
ptr,
|
|
size: BLOCK_OVERHEAD + size, // total incl. overhead
|
|
mmInfo: {
|
|
tags: mmTagsToString[mmInfo & 3],
|
|
size: size // as stored excl. overhead
|
|
},
|
|
gcInfo: {
|
|
color: gcColorToString[gcInfo & 3],
|
|
next: gcInfo & ~3,
|
|
prev: gcInfo2
|
|
},
|
|
rtId,
|
|
rtSize
|
|
};
|
|
}
|
|
|
|
/** Checks if rtrace is active, i.e. at least one event has occurred. */
|
|
get active() {
|
|
return Boolean(this.allocCount || this.resizeCount || this.moveCount || this.freeCount);
|
|
}
|
|
|
|
/** Checks if there are any leaks and emits them via `oninfo`. Returns the number of live blocks. */
|
|
check() {
|
|
if (this.oninfo) {
|
|
for (let [ptr, info] of this.blocks) {
|
|
this.oninfo("LIVE " + ptr + "\n" + info.allocStack.join("\n"));
|
|
}
|
|
}
|
|
return this.blocks.size;
|
|
}
|
|
|
|
// Runtime instrumentation
|
|
|
|
oninit(heapBase) {
|
|
this.heapBase = heapBase;
|
|
this.gcProfileStart = 0;
|
|
this.gcProfile.length = 0;
|
|
this.oninfo("INIT heapBase=" + heapBase);
|
|
}
|
|
|
|
onalloc(ptr) {
|
|
this.syncShadow();
|
|
++this.allocCount;
|
|
var info = this.getBlockInfo(ptr);
|
|
if (this.blocks.has(ptr)) {
|
|
this.onerror(Error("duplicate alloc: " + ptr), info);
|
|
} else {
|
|
this.oninfo("ALLOC " + ptr + ".." + (ptr + info.size));
|
|
this.markShadow(info);
|
|
let allocStack = trimStacktrace(new Error().stack, 1); // strip onalloc
|
|
this.blocks.set(ptr, Object.assign(info, { allocStack }));
|
|
}
|
|
}
|
|
|
|
onresize(ptr, oldSize) {
|
|
this.syncShadow();
|
|
++this.resizeCount;
|
|
const info = this.getBlockInfo(ptr);
|
|
if (!this.blocks.has(ptr)) {
|
|
this.onerror(Error("orphaned resize: " + ptr), info);
|
|
} else {
|
|
const beforeInfo = this.blocks.get(ptr);
|
|
if (beforeInfo.size != oldSize) {
|
|
this.onerror(Error(`size mismatch upon resize: ${ptr} (${beforeInfo.size} != ${oldSize})`), info);
|
|
}
|
|
const newSize = info.size;
|
|
this.oninfo("RESIZE " + ptr + ".." + (ptr + newSize) + " (" + oldSize + "->" + newSize + ")");
|
|
this.blocks.set(ptr, Object.assign(info, { allocStack: beforeInfo.allocStack }));
|
|
if (newSize > oldSize) {
|
|
this.markShadow(info, oldSize);
|
|
} else if (newSize < oldSize) {
|
|
this.unmarkShadow(info, oldSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
onmove(oldPtr, newPtr) {
|
|
this.syncShadow();
|
|
++this.moveCount;
|
|
var oldInfo = this.getBlockInfo(oldPtr);
|
|
var newInfo = this.getBlockInfo(newPtr);
|
|
if (!this.blocks.has(oldPtr)) {
|
|
this.onerror(Error("orphaned move (old): " + oldPtr), oldInfo);
|
|
} else {
|
|
if (!this.blocks.has(newPtr)) {
|
|
this.onerror(Error("orphaned move (new): " + newPtr), newInfo);
|
|
} else {
|
|
const beforeInfo = this.blocks.get(oldPtr);
|
|
const oldSize = oldInfo.size;
|
|
const newSize = newInfo.size;
|
|
if (beforeInfo.size != oldSize) {
|
|
this.onerror(Error(`size mismatch upon move: ${oldPtr} (${beforeInfo.size} != ${oldSize})`), oldInfo);
|
|
}
|
|
this.oninfo("MOVE " + oldPtr + ".." + (oldPtr + oldSize) + " -> " + newPtr + ".." + (newPtr + newSize));
|
|
// calls new alloc before and old free after
|
|
}
|
|
}
|
|
}
|
|
|
|
onvisit(ptr) {
|
|
// Indicates that a block has been freed but it still visited by the GC
|
|
if (ptr > this.heapBase && !this.blocks.has(ptr)) {
|
|
let err = Error("orphaned visit: " + ptr);
|
|
let info = this.freedBlocks.get(ptr);
|
|
if (info) {
|
|
err.stack += "\n^ allocated at:\n" + info.allocStack.join("\n");
|
|
err.stack += "\n^ freed at:\n" + info.freeStack.join("\n");
|
|
}
|
|
this.onerror(err, null);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
onfree(ptr) {
|
|
this.syncShadow();
|
|
++this.freeCount;
|
|
var info = this.getBlockInfo(ptr);
|
|
if (!this.blocks.has(ptr)) {
|
|
this.onerror(Error("orphaned free: " + ptr), info);
|
|
} else {
|
|
const oldInfo = this.blocks.get(ptr);
|
|
if (info.size != oldInfo.size) {
|
|
this.onerror(Error(`size mismatch upon free: ${ptr} (${oldInfo.size} != ${info.size})`), info);
|
|
}
|
|
this.oninfo("FREE " + ptr + ".." + (ptr + info.size));
|
|
this.unmarkShadow(info);
|
|
const allocInfo = this.blocks.get(ptr);
|
|
this.blocks.delete(ptr);
|
|
const allocStack = allocInfo.allocStack;
|
|
const freeStack = trimStacktrace(new Error().stack, 1); // strip onfree
|
|
// (not much) TODO: Maintaining these is essentially a memory leak
|
|
this.freedBlocks.set(ptr, { allocStack, freeStack });
|
|
}
|
|
}
|
|
|
|
oncollect(total) {
|
|
this.oninfo(`COLLECT at ${total}`);
|
|
this.plot(total);
|
|
this.oncollect_();
|
|
}
|
|
|
|
// GC profiling
|
|
|
|
plot(total, pause = 0) {
|
|
if (!this.gcProfileStart) this.gcProfileStart = Date.now();
|
|
this.gcProfile.push([ Date.now() - this.gcProfileStart, total, pause ]);
|
|
}
|
|
|
|
oninterrupt(total) {
|
|
this.interruptStart = hrtime();
|
|
this.plot(total);
|
|
}
|
|
|
|
onyield(total) {
|
|
var pause = hrtime() - this.interruptStart;
|
|
if (pause >= 1) console.log("interrupted for " + pause.toFixed(1) + "ms");
|
|
this.plot(total, pause);
|
|
}
|
|
|
|
// Memory instrumentation
|
|
|
|
onstore(ptr, offset, bytes, isRT) {
|
|
this.accessShadow(ptr + offset, bytes, false, isRT);
|
|
return ptr;
|
|
}
|
|
|
|
onload(ptr, offset, bytes, isRT) {
|
|
this.accessShadow(ptr + offset, bytes, true, isRT);
|
|
return ptr;
|
|
}
|
|
}
|
|
|
|
export default {
|
|
Rtrace
|
|
};
|