Skip to content

Commit

Permalink
perf(codec): improve bytify and hexify perf (#580)
Browse files Browse the repository at this point in the history
  • Loading branch information
homura authored Dec 6, 2023
1 parent 8424b07 commit b8e9396
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/big-ears-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-lumos/codec": patch
---

improving `hexify` and `bytify` performance
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"ava": "^3.8.2",
"benchmark": "^2.1.4",
"c8": "^7.10.0",
"eslint": "^8.40.0",
"eslint-import-resolver-typescript": "^2.7.0",
Expand Down
47 changes: 36 additions & 11 deletions packages/codec/src/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,39 @@ export function bytifyRawString(rawString: string): Uint8Array {
return new Uint8Array(buffer);
}

const CHAR_0 = "0".charCodeAt(0); // 48
const CHAR_9 = "9".charCodeAt(0); // 57
const CHAR_A = "A".charCodeAt(0); // 65
const CHAR_F = "F".charCodeAt(0); // 70
const CHAR_a = "a".charCodeAt(0); // 97
// const CHAR_f = "f".charCodeAt(0); // 102

function bytifyHex(hex: string): Uint8Array {
assertHexString(hex);

hex = hex.slice(2);
const uint8s = [];
for (let i = 0; i < hex.length; i += 2) {
uint8s.push(parseInt(hex.substr(i, 2), 16));
const u8a = Uint8Array.from({ length: hex.length / 2 - 1 });

for (let i = 2, j = 0; i < hex.length; i = i + 2, j++) {
const c1 = hex.charCodeAt(i);
const c2 = hex.charCodeAt(i + 1);

// prettier-ignore
const n1 = c1 <= CHAR_9 ? c1 - CHAR_0 : c1 <= CHAR_F ? c1 - CHAR_A + 10 : c1 - CHAR_a + 10
// prettier-ignore
const n2 = c2 <= CHAR_9 ? c2 - CHAR_0 : c2 <= CHAR_F ? c2 - CHAR_A + 10 : c2 - CHAR_a + 10

u8a[j] = (n1 << 4) | n2;
}

return Uint8Array.from(uint8s);
return u8a;
}

function bytifyArrayLike(xs: ArrayLike<number>): Uint8Array {
const isValidU8Vec = Array.from(xs).every((v) => v >= 0 && v <= 255);
if (!isValidU8Vec) {
throw new Error("invalid ArrayLike, all elements must be 0-255");
for (let i = 0; i < xs.length; i++) {
const v = xs[i];
if (v < 0 || v > 255 || !Number.isInteger(v)) {
throw new Error("invalid ArrayLike, all elements must be 0-255");
}
}

return Uint8Array.from(xs);
Expand Down Expand Up @@ -61,6 +78,10 @@ function equalUint8Array(a: Uint8Array, b: Uint8Array): boolean {
}
return true;
}

const HEX_CACHE = Array.from({ length: 256 }).map((_, i) =>
i.toString(16).padStart(2, "0")
);
/**
* convert a {@link BytesLike} to an even length hex string prefixed with "0x"
* @param buf
Expand All @@ -69,9 +90,13 @@ function equalUint8Array(a: Uint8Array, b: Uint8Array): boolean {
* hexify(Buffer.from([1, 2, 3])) // "0x010203"
*/
export function hexify(buf: BytesLike): string {
const hex = Array.from(bytify(buf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
let hex = "";

const u8a = bytify(buf);
for (let i = 0; i < u8a.length; i++) {
hex += HEX_CACHE[u8a[i]];
}

return "0x" + hex;
}

Expand Down
96 changes: 62 additions & 34 deletions packages/codec/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,76 @@ import {
isCodecExecuteError,
} from "./error";

const HEX_DECIMAL_REGEX = /^0x([0-9a-fA-F])+$/;
const HEX_DECIMAL_WITH_BYTELENGTH_REGEX_MAP = new Map<number, RegExp>();
const CHAR_0 = "0".charCodeAt(0); // 48
const CHAR_9 = "9".charCodeAt(0); // 57
const CHAR_A = "A".charCodeAt(0); // 65
const CHAR_F = "F".charCodeAt(0); // 70
const CHAR_a = "a".charCodeAt(0); // 97
const CHAR_f = "f".charCodeAt(0); // 102

export function assertHexDecimal(str: string, byteLength?: number): void {
if (byteLength) {
let regex = HEX_DECIMAL_WITH_BYTELENGTH_REGEX_MAP.get(byteLength);
if (!regex) {
const newRegex = new RegExp(`^0x([0-9a-fA-F]){1,${byteLength * 2}}$`);
HEX_DECIMAL_WITH_BYTELENGTH_REGEX_MAP.set(byteLength, newRegex);
regex = newRegex;
}
if (!regex.test(str)) {
throw new Error("Invalid hex decimal!");
}
} else {
if (!HEX_DECIMAL_REGEX.test(str)) {
throw new Error("Invalid hex decimal!");
function assertStartsWith0x(str: string): void {
if (!str || !str.startsWith("0x")) {
throw new Error("Invalid hex string");
}
}

function assertHexChars(str: string): void {
const strLen = str.length;

for (let i = 2; i < strLen; i++) {
const char = str[i].charCodeAt(0);
if (
(char >= CHAR_0 && char <= CHAR_9) ||
(char >= CHAR_a && char <= CHAR_f) ||
(char >= CHAR_A && char <= CHAR_F)
) {
continue;
}

throw new Error(`Invalid hex character ${str[i]} in the string ${str}`);
}
}

const HEX_STRING_REGEX = /^0x([0-9a-fA-F][0-9a-fA-F])*$/;
const HEX_STRING_WITH_BYTELENGTH_REGEX_MAP = new Map<number, RegExp>();
export function assertHexDecimal(str: string, byteLength?: number): void {
assertStartsWith0x(str);
if (str.length === 2) {
throw new Error(
"Invalid hex decimal length, should be at least 1 character, the '0x' is incorrect, should be '0x0'"
);
}

const strLen = str.length;

if (typeof byteLength === "number" && strLen > byteLength * 2 + 2) {
throw new Error(
`Invalid hex decimal length, should be less than ${byteLength} bytes, got ${
strLen / 2 - 1
} bytes`
);
}

assertHexChars(str);
}

/**
* Assert if a string is a valid hex string that is matched with /^0x([0-9a-fA-F][0-9a-fA-F])*$/
* @param str
* @param byteLength
*/
export function assertHexString(str: string, byteLength?: number): void {
if (byteLength) {
let regex = HEX_STRING_WITH_BYTELENGTH_REGEX_MAP.get(byteLength);
if (!regex) {
const newRegex = new RegExp(
`^0x([0-9a-fA-F][0-9a-fA-F]){${byteLength}}$`
);
HEX_STRING_WITH_BYTELENGTH_REGEX_MAP.set(byteLength, newRegex);
regex = newRegex;
}
if (!regex.test(str)) {
throw new Error("Invalid hex string!");
}
} else {
if (!HEX_STRING_REGEX.test(str)) {
throw new Error("Invalid hex string!");
}
assertStartsWith0x(str);

const strLen = str.length;

if (strLen % 2) {
throw new Error("Invalid hex string length, must be even!");
}

if (typeof byteLength === "number" && strLen !== byteLength * 2 + 2) {
throw new Error("Invalid hex string length, not match with byteLength!");
}

assertHexChars(str);
}

export function assertUtf8String(str: string): void {
Expand Down
16 changes: 15 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 comments on commit b8e9396

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 New canary release: 0.0.0-canary-b8e9396-20231206155226

npm install @ckb-lumos/[email protected]

@vercel
Copy link

@vercel vercel bot commented on b8e9396 Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.