Skip to content

Commit 2027305

Browse files
added some tests to indentify memory leaks in NetInput and face landmark net + fixed leak in detectLandmarks
1 parent 5f0b196 commit 2027305

File tree

5 files changed

+175
-13
lines changed

5 files changed

+175
-13
lines changed

src/faceLandmarkNet/FaceLandmarkNet.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ export class FaceLandmarkNet {
108108
public async detectLandmarks(input: TNetInput): Promise<FaceLandmarks | FaceLandmarks[]> {
109109
const netInput = await toNetInput(input, true)
110110

111-
const landmarkTensors = tf.unstack(this.forwardInput(netInput))
111+
const landmarkTensors = tf.tidy(
112+
() => tf.unstack(this.forwardInput(netInput))
113+
)
112114

113115
const landmarksForBatch = await Promise.all(landmarkTensors.map(
114116
async (landmarkTensor, batchIdx) => {

test/tests/NetInput.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as tf from '@tensorflow/tfjs-core';
2+
3+
import { NetInput } from '../../src/NetInput';
4+
import { bufferToImage, createCanvasFromMedia } from '../../src/utils';
5+
import { expectAllTensorsReleased, tensor3D } from '../utils';
6+
7+
8+
describe('NetInput', () => {
9+
10+
let imgEl: HTMLImageElement, canvasEl: HTMLCanvasElement
11+
12+
beforeAll(async () => {
13+
const img = await (await fetch('base/test/images/face1.png')).blob()
14+
imgEl = await bufferToImage(img)
15+
canvasEl = createCanvasFromMedia(imgEl)
16+
})
17+
18+
describe('no memory leaks', () => {
19+
20+
describe('constructor', () => {
21+
22+
it('single image element', async () => {
23+
await expectAllTensorsReleased(() => {
24+
const net = new NetInput([imgEl])
25+
net.dispose()
26+
})
27+
})
28+
29+
it('multiple image elements', async () => {
30+
await expectAllTensorsReleased(() => {
31+
const net = new NetInput([imgEl, imgEl, imgEl])
32+
net.dispose()
33+
})
34+
})
35+
36+
it('single tf.Tensor3D', async () => {
37+
const tensor = tensor3D()
38+
39+
await expectAllTensorsReleased(() => {
40+
const net = new NetInput([tensor])
41+
net.dispose()
42+
})
43+
44+
tensor.dispose()
45+
})
46+
47+
it('multiple tf.Tensor3Ds', async () => {
48+
const tensors = [tensor3D(), tensor3D(), tensor3D()]
49+
50+
await expectAllTensorsReleased(() => {
51+
const net = new NetInput(tensors)
52+
net.dispose()
53+
})
54+
55+
tensors.forEach(t => t.dispose())
56+
})
57+
})
58+
59+
describe('toBatchTensor', () => {
60+
61+
it('single image element', async () => {
62+
await expectAllTensorsReleased(() => {
63+
const net = new NetInput([imgEl])
64+
const batchTensor = net.toBatchTensor(100, false)
65+
net.dispose()
66+
batchTensor.dispose()
67+
})
68+
})
69+
70+
it('multiple image elements', async () => {
71+
await expectAllTensorsReleased(() => {
72+
const net = new NetInput([imgEl, imgEl, imgEl])
73+
const batchTensor = net.toBatchTensor(100, false)
74+
net.dispose()
75+
batchTensor.dispose()
76+
})
77+
})
78+
79+
it('managed, single image element', async () => {
80+
await expectAllTensorsReleased(() => {
81+
const net = (new NetInput([imgEl])).managed()
82+
const batchTensor = net.toBatchTensor(100, false)
83+
batchTensor.dispose()
84+
})
85+
})
86+
87+
it('managed, multiple image elements', async () => {
88+
await expectAllTensorsReleased(() => {
89+
const net = (new NetInput([imgEl, imgEl, imgEl])).managed()
90+
const batchTensor = net.toBatchTensor(100, false)
91+
batchTensor.dispose()
92+
})
93+
})
94+
95+
})
96+
97+
})
98+
99+
})

test/tests/e2e/faceLandmarkNet.test.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { isTensor3D } from '../../../src/commons/isTensor';
55
import { FaceLandmarks } from '../../../src/faceLandmarkNet/FaceLandmarks';
66
import { Point } from '../../../src/Point';
77
import { Dimensions, TMediaElement } from '../../../src/types';
8-
import { expectMaxDelta } from '../../utils';
8+
import { expectMaxDelta, expectAllTensorsReleased } from '../../utils';
9+
import { NetInput } from '../../../src/NetInput';
910

1011
function getInputDims (input: tf.Tensor | TMediaElement): Dimensions {
1112
if (input instanceof tf.Tensor) {
@@ -115,7 +116,6 @@ describe('faceLandmarkNet', () => {
115116

116117
})
117118

118-
119119
describe('batch inputs', () => {
120120

121121
let faceLandmarkNet: faceapi.FaceLandmarkNet
@@ -134,6 +134,7 @@ describe('faceLandmarkNet', () => {
134134
faceLandmarkPositions2,
135135
faceLandmarkPositionsRect
136136
]
137+
137138
const results = await faceLandmarkNet.detectLandmarks(inputs) as FaceLandmarks[]
138139
expect(Array.isArray(results)).toBe(true)
139140
expect(results.length).toEqual(3)
@@ -158,6 +159,7 @@ describe('faceLandmarkNet', () => {
158159
faceLandmarkPositions2,
159160
faceLandmarkPositionsRect
160161
]
162+
161163
const results = await faceLandmarkNet.detectLandmarks(inputs) as FaceLandmarks[]
162164
expect(Array.isArray(results)).toBe(true)
163165
expect(results.length).toEqual(3)
@@ -182,6 +184,7 @@ describe('faceLandmarkNet', () => {
182184
faceLandmarkPositions2,
183185
faceLandmarkPositionsRect
184186
]
187+
185188
const results = await faceLandmarkNet.detectLandmarks(tf.stack(inputs) as tf.Tensor4D) as FaceLandmarks[]
186189
expect(Array.isArray(results)).toBe(true)
187190
expect(results.length).toEqual(2)
@@ -207,7 +210,6 @@ describe('faceLandmarkNet', () => {
207210
faceLandmarkPositionsRect
208211
]
209212

210-
211213
const results = await faceLandmarkNet.detectLandmarks(inputs) as FaceLandmarks[]
212214
expect(Array.isArray(results)).toBe(true)
213215
expect(results.length).toEqual(3)
@@ -226,5 +228,51 @@ describe('faceLandmarkNet', () => {
226228

227229
})
228230

231+
describe('no memory leaks', () => {
232+
233+
let faceLandmarkNet: faceapi.FaceLandmarkNet
234+
235+
beforeAll(async () => {
236+
faceLandmarkNet = new faceapi.FaceLandmarkNet()
237+
await faceLandmarkNet.load('base/weights')
238+
})
239+
240+
describe('forwardInput', () => {
241+
242+
it('single image element', async () => {
243+
await expectAllTensorsReleased(async () => {
244+
const netInput = (new NetInput([imgEl1])).managed()
245+
const outTensor = await faceLandmarkNet.forwardInput(netInput)
246+
outTensor.dispose()
247+
})
248+
})
249+
250+
it('multiple image elements', async () => {
251+
await expectAllTensorsReleased(async () => {
252+
const netInput = (new NetInput([imgEl1, imgEl1, imgEl1])).managed()
253+
const outTensor = await faceLandmarkNet.forwardInput(netInput)
254+
outTensor.dispose()
255+
})
256+
})
257+
258+
})
259+
260+
describe('detectLandmarks', () => {
261+
262+
it('single image element', async () => {
263+
await expectAllTensorsReleased(async () => {
264+
await faceLandmarkNet.detectLandmarks(imgEl1)
265+
})
266+
})
267+
268+
it('multiple image elements', async () => {
269+
await expectAllTensorsReleased(async () => {
270+
await faceLandmarkNet.detectLandmarks([imgEl1, imgEl1, imgEl1])
271+
})
272+
})
273+
274+
})
275+
})
276+
229277
})
230278

test/tests/toNetInput.test.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
import { NetInput } from '../../src/NetInput';
22
import { toNetInput } from '../../src/toNetInput';
33
import { bufferToImage, createCanvasFromMedia } from '../../src/utils';
4-
5-
async function createFakeHTMLVideoElement() {
6-
const videoEl = document.createElement('video')
7-
videoEl.muted = true
8-
videoEl.src = 'base/test/media/video.mp4'
9-
await videoEl.pause()
10-
await videoEl.play()
11-
return videoEl
12-
}
4+
import { createFakeHTMLVideoElement } from '../utils';
135

146
describe('toNetInput', () => {
157

test/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as tf from '@tensorflow/tfjs-core';
2+
13
export function zeros(length: number): Float32Array {
24
return new Float32Array(length)
35
}
@@ -9,3 +11,22 @@ export function ones(length: number): Float32Array {
911
export function expectMaxDelta(val1: number, val2: number, maxDelta: number) {
1012
expect(Math.abs(val1 - val2)).toBeLessThan(maxDelta)
1113
}
14+
15+
export async function createFakeHTMLVideoElement() {
16+
const videoEl = document.createElement('video')
17+
videoEl.muted = true
18+
videoEl.src = 'base/test/media/video.mp4'
19+
await videoEl.pause()
20+
await videoEl.play()
21+
return videoEl
22+
}
23+
24+
export async function expectAllTensorsReleased(fn: () => any) {
25+
const numTensorsBefore = tf.memory().numTensors
26+
await fn()
27+
expect(tf.memory().numTensors - numTensorsBefore).toEqual(0)
28+
}
29+
30+
export function tensor3D() {
31+
return tf.tensor3d([[[0]]])
32+
}

0 commit comments

Comments
 (0)