背景
実装中のプログラムでメモリーリークの可能性があったため、解析する方法を必要になった。
疑わしい部分は絞り込めているため、プログラム全体ではなく特定オブジェクトが適切に解放されているかどうかを確認するプログラムを作成した。
実装
弱参照(WeakRef)を用いることで、監視対象のオブジェクトがGCにより回収されたかどうかを監視する。解放済みのオブジェクトを監視し続けることは意味がないので、解放が確認できた弱参照は削除していく
class MemoryLeakChecker {
protected readonly objects: {
[label: string]: {
registered: number,
refs: WeakRef<object>[] // Weak reference to objects
}
} = {}
constructor(public readonly enabled: boolean = true) {
}
register<T extends object>(label: string, object: T): T {
if (this.enabled) {
if (!(label in this.objects)) {
this.objects[label] = {
registered: 0,
refs: []
}
}
this.objects[label].registered++
this.objects[label].refs.push(new WeakRef(object))
}
return object
}
/*Remove WeakRef for released objects*/
removeReleasedRefs() {
if (this.enabled) {
for (const label of Object.keys(this.objects)) {
this.objects[label].refs = this.objects[label].refs.filter(r => r.deref() !== undefined)
}
}
}
summary() {
const items: {
label: string,
registered: number,
alive: number,
freed: number,
}[] = []
if (this.enabled) {
this.removeReleasedRefs()
for (const label of Object.keys(this.objects)) {
const {registered, refs} = this.objects[label]
const alive = refs.length
const freed = registered - refs.length
items.push({
label,
registered,
alive,
freed
})
}
}
return items
}
}
使い方
MemoryLeakCheckerはグローバルに作成し、監視対象となるオブジェクトを登録して利用することを想定している。
メモリ監視が必要ない場合には、MemoryLeakChecker初期化時にfalseを与えることで機能を一元的に無効化できる。
サンプルコード
ya-synのリークチェックのために書いたコード。タスク生成側で作成したオブジェクトと、タスク処理側で作成したオブジェクトをそれぞれ監視している
const checker = new MemoryLeakChecker()
const m = checker.register.bind(checker)
setInterval(() => {
console.table(checker.summary())
}, 1000).unref()
describe("MemoryLeak", () => {
test('Executor', async () => {
const size = 5000000
const sp = new SynchronizerProvider()
const lock = sp.createSynchronizer({
maxConcurrentExecution: 3
})
await sp.executeTasks({
maxTasksInFlight: 10,
maxTasksInExecution: 5,
taskSource: mergeAsyncGenerators([
async function* () {
for (let i = 0; i < 1000; i++) {
yield {
executionId: `${i}`,
task: m("task", new Array(size))
}
}
}(),
]),
taskExecutor: async ({executionId}) => {
await lock.synchronized(async () => {
await new Promise(r => setTimeout(r, 1000))
console.log(`${executionId}:executed`)
return m("result", new Array(size))
})
}
})
}, 1000 * 60 * 60);
})
実行結果
console.log
┌─────────┬──────────┬────────────┬───────┬───────┐
│ (index) │ label │ registered │ alive │ freed │
├─────────┼──────────┼────────────┼───────┼───────┤
│ 0 │ 'task' │ 34 │ 14 │ 20 │
│ 1 │ 'result' │ 24 │ 4 │ 20 │
└─────────┴──────────┴────────────┴───────┴───────┘
at Timeout._onTimeout (test/MemoryLeak.test.ts:66:13)
gcを直接読んだ場合
expose-gcでgcを利用できるように設定する
NODE_OPTIONS="--expose-gc" npm test test/MemoryLeak.test.ts
毎回GCした場合の実行ログ
処理中のメッセージ10件以外は綺麗に解放されている
┌─────────┬────────────┬────────────┬───────┬───────┐
│ (index) │ label │ registered │ alive │ freed │
├─────────┼────────────┼────────────┼───────┼───────┤
│ 0 │ 'register' │ 43 │ 10 │ 33 │
│ 1 │ 'execute' │ 33 │ 0 │ 33 │
└─────────┴────────────┴────────────┴───────┴───────┘