• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16declare function requireNapi(napiModuleName: string): any;
17declare function registerArkUIObjectLifeCycleCallback(callback: (weakRef: WeakRef<object>, msg: string) => void);
18declare function unregisterArkUIObjectLifeCycleCallback();
19
20let util = requireNapi('util');
21let fs = requireNapi('file.fs');
22let hidebug = requireNapi('hidebug');
23let cryptoFramework = requireNapi('security.cryptoFramework');
24let jsLeakWatcherNative = requireNapi('hiviewdfx.jsleakwatchernative');
25
26const ERROR_CODE_INVALID_PARAM = 401;
27const ERROR_MSG_INVALID_PARAM = 'Parameter error. Please check!';
28const ERROR_CODE_ENABLE_INVALID = 10801001;
29const ERROR_MSG_ENABLE_INVALID = 'The parameter isEnabled invalid. Please check!';
30const ERROR_CODE_CONFIG_INVALID = 10801002;
31const ERROR_MSG_CONFIG_INVALID = 'The parameter config invalid. Please check!';
32const ERROR_CODE_CALLBACK_INVALID = 10801003;
33const ERROR_MSG_CALLBACK_INVALID = 'The parameter callback invalid. Please check!';
34
35
36let errMap = new Map();
37errMap.set(ERROR_CODE_INVALID_PARAM, ERROR_MSG_INVALID_PARAM);
38errMap.set(ERROR_CODE_ENABLE_INVALID, ERROR_MSG_ENABLE_INVALID);
39errMap.set(ERROR_CODE_CONFIG_INVALID, ERROR_MSG_CONFIG_INVALID);
40errMap.set(ERROR_CODE_CALLBACK_INVALID, ERROR_MSG_CALLBACK_INVALID);
41
42class BusinessError extends Error {
43  constructor(code) {
44    let msg = '';
45    if (errMap.has(code)) {
46      msg = errMap.get(code);
47    } else {
48      msg = ERROR_MSG_INVALID_PARAM;
49    }
50    super(msg);
51    this.code = code;
52  }
53}
54
55let enabled = false;
56let watchObjMap = new Map();
57
58const registry = new FinalizationRegistry((hash) => {
59  if (watchObjMap.has(hash)) {
60    watchObjMap.delete(hash);
61  }
62});
63const MAX_FILE_NUM = 20;
64
65function getLeakList() {
66  return Array.from(watchObjMap.values());
67}
68
69function createHeapDumpFile(fileName, filePath, isRawHeap) {
70  let suffix = isRawHeap ? '.rawheap' : '.heapsnapshot';
71  let heapDumpFileName = fileName + suffix;
72  let desFilePath = filePath + '/' + heapDumpFileName;
73  if (isRawHeap) {
74    jsLeakWatcherNative.dumpRawHeap(desFilePath);
75  } else {
76    hidebug.dumpJsHeapData(fileName);
77    fs.moveFileSync('/data/storage/el2/base/files/' + heapDumpFileName, desFilePath, 0);
78  }
79  return getHeapDumpSHA256(desFilePath);
80}
81
82function getHeapDumpSHA256(filePath) {
83  let md = cryptoFramework.createMd('SHA256');
84  let heapDumpFile = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
85  let bufSize = 40960;
86  let readSize = 0;
87  let buf = new ArrayBuffer(bufSize);
88  let readOptions = { offset: readSize, length: bufSize };
89  let readLen = fs.readSync(heapDumpFile.fd, buf, readOptions);
90
91  while (readLen > 0) {
92    md.updateSync({ data: new Uint8Array(buf.slice(0, readLen)) });
93    readSize += readLen;
94    readOptions.offset = readSize;
95    readLen = fs.readSync(heapDumpFile.fd, buf, readOptions);
96  }
97  fs.closeSync(heapDumpFile);
98
99  let digestOutPut = md.digestSync();
100  return Array.from(digestOutPut.data, byte => ('0' + byte.toString(16)).slice(-2)).join('').toUpperCase();
101}
102
103function deleteOldFile(filePath) {
104  let listFileOption = {
105    recursion: false,
106    listNum: 0,
107    filter: {
108      displayName: ['*.heapsnapshot', '*.jsleaklist', '*.rawheap'],
109    }
110  };
111  let files = fs.listFileSync(filePath, listFileOption);
112  if (files.length > MAX_FILE_NUM) {
113    const regex = /(\d+)\.(heapsnapshot|jsleaklist|rawheap)/;
114    files.sort((a, b) => {
115      const matchA = a.match(regex);
116      const matchB = b.match(regex);
117      const timeStampA = matchA ? parseInt(matchA[1]) : 0;
118      const timeStampB = matchB ? parseInt(matchB[1]) : 0;
119      return timeStampA - timeStampB;
120    });
121    for (let i = 0; i < files.length - MAX_FILE_NUM; i++) {
122      fs.unlinkSync(filePath + '/' + files[i]);
123      console.log(`File: ${files[i]} is deleted.`);
124    }
125  }
126}
127
128function registerObject(obj, msg) {
129  if (!obj) {
130    return;
131  }
132  let objMsg = { hash: util.getHash(obj), name: obj.constructor.name, msg: msg };
133  watchObjMap.set(objMsg.hash, objMsg);
134  registry.register(obj, objMsg.hash);
135}
136
137let lifecycleId;
138function registerAbilityLifecycleCallback() {
139  let abilityLifecycleCallback = {
140    onAbilityDestroy(ability) {
141      registerObject(ability, '');
142    }
143  }
144  const context : Context = getContext(this);
145  if (context) {
146    let applicationContext = context.getApplicationContext();
147    lifecycleId = applicationContext.registerAbilityLifecycleCallback(abilityLifecycleCallback);
148  }
149}
150
151function unregisterAbilityLifecycleCallback() {
152  const context : Context = getContext(this);
153  if (context) {
154    let applicationContext = context.getApplicationContext();
155    applicationContext.unregisterAbilityLifecycleCallback(lifecycleId, (error, data) => {
156      console.log('unregisterAbilityLifecycleCallback success! err:' + JSON.stringify(error));
157    });
158  }
159}
160
161function executeRegister(config: Array<string>) {
162  if (config.includes('CustomComponent')) {
163    registerArkUIObjectLifeCycleCallback((weakRef, msg) => {
164      if (!weakRef) {
165        return;
166      }
167      let obj = weakRef.deref();
168      registerObject(obj, msg);
169    });
170  }
171  if (config.includes('Window')) {
172    jsLeakWatcherNative.registerWindowLifeCycleCallback((obj) => {
173      registerObject(obj, '');
174    });
175  }
176  if (config.includes('NodeContainer') || config.includes('XComponent')) {
177    jsLeakWatcherNative.registerArkUIObjectLifeCycleCallback((weakRef) => {
178      if (!weakRef) {
179        return;
180      }
181      let obj = weakRef.deref();
182      registerObject(obj, '');
183    });
184  }
185  if (config.includes('Ability')) {
186    registerAbilityLifecycleCallback();
187  }
188}
189
190function dumpInner(filePath, needSandBox, isRawHeap) {
191  if (!enabled) {
192    return [];
193  }
194  if (!fs.accessSync(filePath, fs.AccessModeType.EXIST)) {
195    throw new BusinessError(ERROR_CODE_INVALID_PARAM);
196  }
197  const fileTimeStamp = new Date().getTime().toString();
198  try {
199    const heapDumpSHA256 = createHeapDumpFile(fileTimeStamp, filePath, isRawHeap);
200    let file = fs.openSync(filePath + '/' + fileTimeStamp + '.jsleaklist', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
201    let leakObjList = getLeakList();
202    if (isRawHeap) {
203      let result = { version: '2.0.0', snapshot_hash: heapDumpSHA256, leakObjList: leakObjList };
204      fs.writeSync(file.fd, JSON.stringify(result));
205    } else {
206      let result = { snapshot_hash: heapDumpSHA256, leakObjList: leakObjList };
207      fs.writeSync(file.fd, JSON.stringify(result));
208    }
209    fs.closeSync(file);
210  } catch (error) {
211    console.log('Dump heaoSnapShot or LeakList failed! ' + error);
212    return [];
213  }
214
215  try {
216    deleteOldFile(filePath);
217  } catch (e) {
218    console.log('Delete old files failed! ' + e);
219    return [];
220  }
221  if (needSandBox) {
222    return [filePath + '/' + fileTimeStamp + '.jsleaklist', filePath + '/' + fileTimeStamp + '.rawheap'];
223  } else {
224    return [fileTimeStamp + '.jsleaklist', fileTimeStamp + '.heapsnapshot'];
225  }
226}
227
228function shutdownJsLeakWatcher() {
229  jsLeakWatcherNative.removeTask();
230  jsLeakWatcherNative.unregisterArkUIObjectLifeCycleCallback();
231  jsLeakWatcherNative.unregisterWindowLifeCycleCallback();
232  unregisterArkUIObjectLifeCycleCallback();
233  unregisterAbilityLifecycleCallback();
234  watchObjMap.clear();
235}
236
237let jsLeakWatcher = {
238  watch: (obj, msg) => {
239    if (obj === undefined || obj === null || msg === undefined || msg === null) {
240      throw new BusinessError(ERROR_CODE_INVALID_PARAM);
241    }
242    if (!enabled) {
243      return;
244    }
245    let objMsg = { hash: util.getHash(obj), name: obj.constructor.name, msg: msg };
246    watchObjMap.set(objMsg.hash, objMsg);
247    registry.register(obj, objMsg.hash);
248  },
249  check: () => {
250    if (!enabled) {
251      return '';
252    }
253    let leakObjList = getLeakList();
254    return JSON.stringify(leakObjList);
255  },
256  dump: (filePath) => {
257    if (filePath === undefined || filePath === null) {
258      throw new BusinessError(ERROR_CODE_INVALID_PARAM);
259    }
260    return dumpInner(filePath, false, false);
261  },
262  enable: (isEnable) => {
263    if (isEnable === undefined || isEnable === null) {
264      throw new BusinessError(ERROR_CODE_INVALID_PARAM);
265    }
266    enabled = isEnable;
267    if (!isEnable) {
268      watchObjMap.clear();
269    }
270  },
271  enableLeakWatcher: (isEnable: boolean, config: Array<string>, callback: Callback<Array<string>>) => {
272    if (isEnable === undefined || isEnable === null) {
273      throw new BusinessError(ERROR_CODE_ENABLE_INVALID);
274    }
275    if (config === undefined || config === null) {
276      throw new BusinessError(ERROR_CODE_CONFIG_INVALID);
277    }
278    if (callback === undefined || callback === null) {
279      throw new BusinessError(ERROR_CODE_CALLBACK_INVALID);
280    }
281    if (isEnable === enabled) {
282      console.log('JsLeakWatcher is already started or stopped.');
283      return;
284    }
285    enabled = isEnable;
286    if (!isEnable) {
287      shutdownJsLeakWatcher();
288      return;
289    }
290
291    const validConfig = ['CustomComponent', 'Window', 'NodeContainer', 'XComponent', 'Ability'];
292    for (let i = 0; i < config.length; i++) {
293      if (!validConfig.includes(config[i])) {
294        throw new BusinessError(ERROR_CODE_CONFIG_INVALID);
295      }
296    }
297    if (config.length === 0) {
298      config = validConfig;
299    }
300
301    const context : Context = getContext(this);
302    const filePath : string = context ? context.filesDir : '/data/storage/el2/base/files/';
303
304    jsLeakWatcherNative.handleGCTask(() => {
305      ArkTools.forceFullGC();
306    });
307    jsLeakWatcherNative.handleDumpTask(() => {
308      if (watchObjMap.size === 0) {
309        console.log('No js leak detected, no need to dump.');
310        return;
311      }
312      let fileArray = dumpInner(filePath, true, true);
313      callback(fileArray);
314    });
315    jsLeakWatcherNative.handleShutdownTask(() => {
316      enabled = false;
317      shutdownJsLeakWatcher();
318    });
319    executeRegister(config);
320  }
321};
322
323export default jsLeakWatcher;
324