* Copyright (C) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
const protobuf = require('protobufjs');
const {
LONG_TRACE_HEADER_MAGIC,
LONG_TRACE_HEADER_SIZE_BYTES,
LONG_TRACE_PLUGIN_NAME_NATIVE_HOOK,
LONG_TRACE_PLUGIN_STATUS_OK,
LONG_TRACE_PROTO_VERSION,
LONG_TRACE_PROTO_TYPE_BATCH_NATIVE_HOOK_DATA,
LONG_TRACE_PROTO_TYPE_NATIVE_HOOK_DATA,
LONG_TRACE_PROTO_TYPE_PROFILER_PLUGIN_DATA,
LONG_TRACE_SEGMENT_LENGTH_BYTES,
LONG_TRACE_TEXT_ENCODING_UTF8,
} = require('./traceConstants.js');
const NANOSECONDS_PER_SECOND = BigInt(1000000000);
* 创建一个 native hook 处理器实例,用于解析和处理 native hook 数据。
* @returns {Object} - 包含解析和处理 native hook 数据的方法的对象。
*/
function createNativeHookProcessor() {
let longTraceProtoRoot;
let longTraceProfilerPluginDataType;
let longTraceBatchNativeHookDataType;
let longTraceNativeHookDataType;
* 首次使用时懒加载 proto 元信息,避免服务启动阶段承担不必要的 I/O 开销。
* @returns {void}
* @description 无返回值。
*/
function ensureLongTraceProtoLoaded() {
if (longTraceProtoRoot) {
return;
}
const commonTypesProto = path.join(__dirname, '..', 'proto', 'common_types.proto');
const nativeHookProto = path.join(__dirname, '..', 'proto', 'native_hook_result.proto');
longTraceProtoRoot = protobuf.loadSync([commonTypesProto, nativeHookProto]);
longTraceProfilerPluginDataType = longTraceProtoRoot.lookupType(LONG_TRACE_PROTO_TYPE_PROFILER_PLUGIN_DATA);
longTraceBatchNativeHookDataType = longTraceProtoRoot.lookupType(LONG_TRACE_PROTO_TYPE_BATCH_NATIVE_HOOK_DATA);
longTraceNativeHookDataType = longTraceProtoRoot.lookupType(LONG_TRACE_PROTO_TYPE_NATIVE_HOOK_DATA);
}
* 统把 long/number/string 等输入转换为 BigInt,便于后续做时间戳和大小比较。
* @param {*} value - 输入值,可能是是 bigint、number、string 或其他对象。
* @returns {bigint} - 转换后的 BigInt 值。
*/
function toBigIntU64(value) {
if (typeof value === 'bigint') {
return value;
}
if (typeof value === 'number' && Number.isFinite(value)) {
return BigInt(value);
}
if (typeof value === 'string' && value) {
return BigInt(value);
}
if (value && typeof value.toString === 'function') {
return BigInt(value.toString());
}
return BigInt(0);
}
* 校验 header 长度和 magic,快速过滤掉非法 trace 输入。
* @param {Buffer} traceBuffer - 输入的 trace 字节流。
* @returns {boolean} - 是否为合法的 trace 输入。
*/
function isValidHtraceHeader(traceBuffer) {
if (!traceBuffer || traceBuffer.byteLength < LONG_TRACE_HEADER_SIZE_BYTES) {
return false;
}
try {
return traceBuffer.readBigUInt64LE(0) === LONG_TRACE_HEADER_MAGIC;
} catch (e) {
return false;
}
}
* 按 htrace 的“长度 + payload”格式顺序遍历每个 segment。
* @param {Buffer} traceBuffer - 输入的 trace 字节流。
* @param {function} onSegmentPayload - 每个 segment 的 payload 回调函数,参数为 segment 的 payload 字节流。
*/
function forEachHtraceSegment(traceBuffer, onSegmentPayload) {
let offset = LONG_TRACE_HEADER_SIZE_BYTES;
while (offset + LONG_TRACE_SEGMENT_LENGTH_BYTES <= traceBuffer.byteLength) {
const segmentLength = traceBuffer.readUInt32LE(offset);
offset += LONG_TRACE_SEGMENT_LENGTH_BYTES;
if (segmentLength <= 0 || offset + segmentLength > traceBuffer.byteLength) {
break;
}
onSegmentPayload(traceBuffer.subarray(offset, offset + segmentLength));
offset += segmentLength;
}
}
* 快速探测原始字节流中是否包含 nativehook 关键字,用于尽早短路无关文件。
* @param {Buffer} traceBuffer - 输入的 trace 字节流。
* @returns {boolean} - 是否包含 nativehook 关键字。
*/
function hasNativehookFast(traceBuffer) {
if (!traceBuffer || traceBuffer.byteLength <= LONG_TRACE_HEADER_SIZE_BYTES) {
return false;
}
try {
const needle = Buffer.from(LONG_TRACE_PLUGIN_NAME_NATIVE_HOOK, LONG_TRACE_TEXT_ENCODING_UTF8);
return traceBuffer.indexOf(needle, LONG_TRACE_HEADER_SIZE_BYTES) !== -1;
} catch (e) {
return false;
}
}
* 仅保留 nativehook 插件段,降低后续 protobuf 解码的内存和 CPU 压力。
* @param {Buffer} traceBuffer - 输入的 trace 字节流。
* @returns {Buffer} - 过滤后的 trace 字节流,仅包含 nativehook 插件段。
*/
function filterNativeHookSegments(traceBuffer) {
if (!isValidHtraceHeader(traceBuffer)) {
return traceBuffer;
}
ensureLongTraceProtoLoaded();
const chunks = [traceBuffer.subarray(0, LONG_TRACE_HEADER_SIZE_BYTES)];
let offset = LONG_TRACE_HEADER_SIZE_BYTES;
while (offset + LONG_TRACE_SEGMENT_LENGTH_BYTES <= traceBuffer.byteLength) {
const segmentLength = traceBuffer.readUInt32LE(offset);
const segmentStart = offset;
offset += LONG_TRACE_SEGMENT_LENGTH_BYTES;
if (segmentLength <= 0 || offset + segmentLength > traceBuffer.byteLength) {
break;
}
const segmentPayload = traceBuffer.subarray(offset, offset + segmentLength);
try {
const plugin = longTraceProfilerPluginDataType.decode(segmentPayload);
if (plugin.name === LONG_TRACE_PLUGIN_NAME_NATIVE_HOOK) {
chunks.push(traceBuffer.subarray(segmentStart, offset + segmentLength));
}
} catch (e) {}
offset += segmentLength;
}
return Buffer.concat(chunks);
}
* 扫描所有 nativehook 段,找出时间线最早的事件时间用于补丁对齐。
* @param {Buffer} traceBuffer - 输入的 trace 字节流。
* @returns {object} - 包含 clockId、tvSec、tvNsec 的对象,若未找到则返回 undefined 。
*/
function findEarliestNativehookTimestamp(traceBuffer) {
if (!isValidHtraceHeader(traceBuffer)) {
return undefined;
}
ensureLongTraceProtoLoaded();
let minTs;
let minClockId;
let minTvSec;
let minTvNsec;
forEachHtraceSegment(traceBuffer, (segmentPayload) => {
let plugin;
try {
plugin = longTraceProfilerPluginDataType.decode(segmentPayload);
} catch (e) {
return;
}
if (plugin.name !== LONG_TRACE_PLUGIN_NAME_NATIVE_HOOK) {
return;
}
const tvSec = toBigIntU64(plugin.tvSec);
const tvNsec = toBigIntU64(plugin.tvNsec);
const ts = tvSec * NANOSECONDS_PER_SECOND + tvNsec;
if (minTs === undefined || ts < minTs) {
minTs = ts;
minClockId = plugin.clockId;
minTvSec = tvSec;
minTvNsec = tvNsec;
}
});
if (minTs === undefined) {
return undefined;
}
return { clockId: minClockId, tvSec: minTvSec, tvNsec: minTvNsec };
}
* 根据 oneof 展开后的字段判断当前事件属于公共上下文还是统计事件。
* @param {object} eventObj - 包含事件数据的对象。
* @returns {string} - 事件类型,'commonData' 或 'statisticsEvent',若无法判断则返回 undefined 。
*/
function detectNativeHookEventField(eventObj) {
if (
eventObj.symbolTab ||
eventObj.mapsInfo ||
eventObj.threadNameMap ||
eventObj.symbolName ||
eventObj.filePath ||
eventObj.tagEvent ||
eventObj.frameMap ||
eventObj.stackMap
) {
return 'commonData';
}
if (eventObj.statisticsEvent) {
return 'statisticsEvent';
}
return undefined;
}
* 解码单个 nativehook 段中的 BatchNativeHookData 事件数组。
* @param {Buffer} segmentPayload - 输入的 trace 字节流。
* @returns {Array} - 包含事件数据的数组,若解码失败则返回 null 。
*/
function decodeNativeHookEvents(segmentPayload) {
let plugin;
try {
plugin = longTraceProfilerPluginDataType.decode(segmentPayload);
} catch (e) {
return null;
}
if (plugin.name !== LONG_TRACE_PLUGIN_NAME_NATIVE_HOOK) {
return null;
}
let batch;
try {
batch = longTraceBatchNativeHookDataType.decode(plugin.data);
} catch (e) {
return null;
}
return Array.isArray(batch.events) ? batch.events : [];
}
* 按 pid + callstackId 聚合统计事件,只保留最新且仍有残留分配的数据。
* @param {Array} commonEvents - 包含公共事件数据的数组。
* @param {Array} statisticsEvents - 包含统计事件数据的数组。
* @returns {Array} - 合并后的公共事件数组,包含最新且仍有残留分配的统计事件。
*/
function mergeStatistics(commonEvents, statisticsEvents) {
if (statisticsEvents.length <= 0) {
return commonEvents;
}
const latestByKey = new Map();
for (const event of statisticsEvents) {
const stats = event.statisticsEvent;
if (!stats) {
continue;
}
const key = `${Number(stats.pid)}-${Number(stats.callstackId)}`;
const ts = toBigIntU64(event.tvSec) * NANOSECONDS_PER_SECOND + toBigIntU64(event.tvNsec);
const prev = latestByKey.get(key);
if (!prev || ts > prev.ts) {
latestByKey.set(key, { ts, event });
}
}
for (const { event } of latestByKey.values()) {
const stats = event.statisticsEvent;
if (!stats) {
continue;
}
const applySize = toBigIntU64(stats.applySize);
const releaseSize = toBigIntU64(stats.releaseSize);
if (applySize > releaseSize) {
commonEvents.push(event);
}
}
return commonEvents;
}
* 抽取跨页补丁所需的公共事件,为后续页构造 NativeHook 上下文。
* @param {Buffer} traceBuffer - 输入的 trace 字节流。
* @returns {Array} - 包含公共事件数据的数组。
*/
function extractCommonEvents(traceBuffer) {
if (!isValidHtraceHeader(traceBuffer)) {
return [];
}
ensureLongTraceProtoLoaded();
const commonEvents = [];
const statisticsEvents = [];
forEachHtraceSegment(traceBuffer, (segmentPayload) => {
const events = decodeNativeHookEvents(segmentPayload);
if (!events) {
return;
}
for (const ev of events) {
const obj = longTraceNativeHookDataType.toObject(ev, { longs: String, bytes: Buffer, defaults: false });
const field = detectNativeHookEventField(obj);
if (!field) {
continue;
}
if (field === 'statisticsEvent') {
statisticsEvents.push(obj);
} else {
commonEvents.push(obj);
}
}
});
return mergeStatistics(commonEvents, statisticsEvents);
}
* 用目标页时间戳重写公共事件后重新编码为可插入 trace 的补丁段。
* @param {Array} commonEvents - 包含公共事件数据的数组。
* @param {string} clockId - 目标页的 clockId 字符串。
* @param {string} tvSec - 目标页的时间戳秒部分字符串。
* @param {string} tvNsec - 目标页的时间戳纳秒部分字符串。
* @returns {Buffer} - 可插入 trace 的补丁段字节流。
*/
function buildNativehookPatchPayload(commonEvents, clockId, tvSec, tvNsec) {
ensureLongTraceProtoLoaded();
const tvSecStr = toBigIntU64(tvSec).toString();
const tvNsecStr = toBigIntU64(tvNsec).toString();
for (let i = 0; i < commonEvents.length; i++) {
commonEvents[i].tvSec = tvSecStr;
commonEvents[i].tvNsec = tvNsecStr;
}
const batchBytes = longTraceBatchNativeHookDataType.encode({ events: commonEvents }).finish();
const pluginBytes = longTraceProfilerPluginDataType.encode({
name: LONG_TRACE_PLUGIN_NAME_NATIVE_HOOK,
status: LONG_TRACE_PLUGIN_STATUS_OK,
data: batchBytes,
clockId: clockId,
tvSec: tvSecStr,
tvNsec: tvNsecStr,
version: LONG_TRACE_PROTO_VERSION,
}).finish();
const lenBuf = Buffer.alloc(LONG_TRACE_SEGMENT_LENGTH_BYTES);
lenBuf.writeUInt32LE(pluginBytes.length, 0);
return Buffer.concat([lenBuf, Buffer.from(pluginBytes)]);
}
return {
buildNativehookPatchPayload,
extractCommonEvents,
filterNativeHookSegments,
findEarliestNativehookTimestamp,
hasNativehookFast,
headerSizeBytes: LONG_TRACE_HEADER_SIZE_BYTES,
};
}
module.exports = {
createNativeHookProcessor,
};