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