Comparing version
@@ -15,6 +15,4 @@ { | ||
"./transform" | ||
], | ||
"bindings": "esm" | ||
}, | ||
"extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json" | ||
] | ||
} | ||
} |
import { JSON } from ".."; | ||
import { backSlashCode, quoteCode } from "../src/chars"; | ||
import { atoi_fast, unsafeCharCodeAt } from "../src/util"; | ||
@@ -8,43 +10,81 @@ @json | ||
z: i32; | ||
/*@inline __JSON_Serialize(data: Vec3): string { | ||
return `{"x":${data.x.toString()},"y":${data.y.toString()},"z":${data.z.toString()}}`; | ||
}*/ | ||
@inline __JSON_Deserialize(data: string, to: Vec3): Vec3 { | ||
let last = 1; | ||
let char = 0; | ||
let inStr = false; | ||
let key: string | null = null; | ||
let pos = 0; | ||
for (; pos < data.length - 1; pos++) { | ||
char = unsafeCharCodeAt(data, pos); | ||
if (inStr === false && char === quoteCode) { | ||
if (key != null) { | ||
if (unsafeCharCodeAt(key, 0) == 120) { | ||
to.x = atoi_fast<i32>(data.slice(last, pos - 1)) | ||
} else if (unsafeCharCodeAt(key, 0) == 121) { | ||
to.y = atoi_fast<i32>(data.slice(last, pos - 1)) | ||
} else if (unsafeCharCodeAt(key, 0) == 122) { | ||
to.z = atoi_fast<i32>(data.slice(last, pos - 1)) | ||
} | ||
} | ||
last = ++pos; | ||
inStr = true; | ||
} else if (char === quoteCode && unsafeCharCodeAt(data, pos - 1) != backSlashCode) { | ||
inStr = false; | ||
key = data.slice(last, pos); | ||
last = pos += 2; | ||
} | ||
} | ||
if (key != null) { | ||
if (unsafeCharCodeAt(key, 0) == 120) { | ||
to.x = atoi_fast<i32>(data.slice(last, pos - 1)) | ||
} else if (unsafeCharCodeAt(key, 0) == 121) { | ||
to.y = atoi_fast<i32>(data.slice(last, pos - 1)) | ||
} else if (unsafeCharCodeAt(key, 0) == 122) { | ||
to.z = atoi_fast<i32>(data.slice(last, pos - 1)) | ||
} | ||
} | ||
return to; | ||
} | ||
} | ||
const vec: Vec3 = blackbox<Vec3>({ | ||
x: 0, | ||
y: 0, | ||
z: 0 | ||
}); | ||
const vec: Vec3 = { | ||
x: 3, | ||
y: 1, | ||
z: 8 | ||
}; | ||
const vecOut = new Vec3(); | ||
const i32Max = blackbox("429496729"); | ||
/* | ||
bench("Stringify Object (Vec3)", () => { | ||
blackbox(JSON.stringify(vec)); | ||
}); | ||
blackbox<string>(vec.__JSON_Serialize(vec)); | ||
});*/ | ||
// TODO: Make this allocate without crashing | ||
bench("Parse Object (Vec3)", () => { | ||
blackbox(JSON.parse<Vec3>(blackbox('{"x":0,"y":0,"z":0}'))); | ||
});/* | ||
blackbox<Vec3>(vec.__JSON_Deserialize('{"x":0,"y":0,"z":0}', vec)); | ||
}); | ||
bench("Stringify Array", () => { | ||
blackbox(JSON.stringify(blackbox([1, 2, 3, 4, 5]))); | ||
bench("Stringify Number Array", () => { | ||
blackbox(JSON.stringify<i32[]>([1, 2, 3])); | ||
}); | ||
bench("Stringify String Array", () => { | ||
blackbox(JSON.stringify(blackbox(["a", "b", "c", "d", "e"]))); | ||
}); | ||
/* | ||
bench("Parse Array", () => { | ||
blackbox(JSON.parse<i32[]>(blackbox("[1,2,3,4]"))); | ||
blackbox(JSON.parse<i32[]>(blackbox("[1,2,3]"))); | ||
}); | ||
bench("Stringify Nested Array", () => { | ||
blackbox( | ||
JSON.stringify<string[][]>( | ||
blackbox([ | ||
["a", "b", "c"] | ||
]) | ||
) | ||
); | ||
bench("Stringify Boolean Array", () => { | ||
blackbox(JSON.stringify<boolean[]>([true, false, true])); | ||
}); | ||
bench("Parse Nested Array", () => { | ||
blackbox(JSON.parse<string[][]>(blackbox('[["a","b","c"]]'))); | ||
bench("Stringify String Array", () => { | ||
blackbox(JSON.stringify<string[]>(["a", "b", "c"])); | ||
}); | ||
*/ | ||
bench("Stringify String", () => { | ||
@@ -57,3 +97,3 @@ blackbox(JSON.stringify(blackbox("Hello \"World!"))); | ||
}); | ||
/* | ||
bench("Stringify Boolean", () => { | ||
@@ -81,2 +121,2 @@ blackbox(JSON.stringify(blackbox(true))); | ||
blackbox(JSON.parse<f32>(blackbox("3.14"))); | ||
});*/ | ||
}); |
@@ -26,3 +26,3 @@ import { u128, u128Safe, u256, u256Safe, i128, i128Safe } from "as-bignum/assembly"; | ||
} from "./chars"; | ||
import { escapeChar, isBigNum, unsafeCharCodeAt } from "./util"; | ||
import { atoi_fast, escapeChar, isBigNum, unsafeCharCodeAt } from "./util"; | ||
@@ -46,30 +46,17 @@ /** | ||
return serializeString(data); | ||
} | ||
// Boolean | ||
else if (isBoolean<T>()) { | ||
} else if (isBoolean<T>()) { | ||
return data ? "true" : "false"; | ||
} | ||
// Nullable | ||
else if (isNullable<T>() && data == null) { | ||
} else if (isNullable<T>() && data == null) { | ||
return "null"; | ||
} | ||
// Integers/Floats | ||
// @ts-ignore | ||
else if ((isInteger<T>() || isFloat<T>()) && isFinite(data)) { | ||
// @ts-ignore | ||
} else if ((isInteger<T>() || isFloat<T>()) && isFinite(data)) { | ||
// @ts-ignore | ||
return data.toString(); | ||
} | ||
// Class-Based serialization | ||
// @ts-ignore | ||
else if (isDefined(data.__JSON_Serialize)) { | ||
// @ts-ignore | ||
//if (isNullable<T>()) return "null"; | ||
} else if (isDefined(data.__JSON_Serialize)) { | ||
// @ts-ignore | ||
return data.__JSON_Serialize(); | ||
} | ||
else if (data instanceof Date) { | ||
} else if (data instanceof Date) { | ||
return data.toISOString(); | ||
} | ||
// ArrayLike | ||
else if (isArrayLike<T>()) { | ||
} else if (isArrayLike<T>()) { | ||
// @ts-ignore | ||
@@ -92,14 +79,9 @@ if (data.length == 0) { | ||
// @ts-ignore | ||
} else if (isBoolean<valueof<T>>()) { | ||
// @ts-ignore | ||
return leftBracketWord + data.join(commaWord) + rightBracketWord; | ||
// @ts-ignore | ||
} else if (isFloat<valueof<T>>() || isInteger<valueof<T>>()) { | ||
let result = new StringSink(leftBracketWord); | ||
// @ts-ignore | ||
for (let i = 0; i < data.length - 1; i++) { | ||
// @ts-ignore | ||
result.write(JSON.stringify(unchecked(data[i]))); | ||
result.write(commaWord); | ||
} | ||
// @ts-ignore | ||
result.write(JSON.stringify(unchecked(data[data.length - 1]))); | ||
result.write(rightBracketWord); | ||
return result.toString(); | ||
return leftBracketWord + data.join(commaWord) + rightBracketWord; | ||
} else { | ||
@@ -170,3 +152,44 @@ let result = new StringSink(leftBracketWord); | ||
// @ts-ignore | ||
return data.replaceAll('\\"', '"'); | ||
let result = ""; | ||
let last = 0; | ||
let char = 0; | ||
for (let i = 0; i < data.length; i++) { | ||
// \\" | ||
if (unsafeCharCodeAt(data, i) === backSlashCode) { | ||
char = unsafeCharCodeAt(data, ++i); | ||
result += data.slice(last, i - 1) | ||
if (char === 34) { | ||
result += "\""; | ||
last = ++i; | ||
} else if (char === 110) { | ||
result += "\n"; | ||
last = ++i; | ||
// 92 98 114 116 102 117 | ||
} else if (char >= 92 && char <= 117) { | ||
if (char === 92) { | ||
result += "\\"; | ||
last = ++i; | ||
} else if (char === 98) { | ||
result += "\b"; | ||
last = ++i; | ||
} else if (char === 102) { | ||
result += "\f"; | ||
last = ++i; | ||
} else if (char === 114) { | ||
result += "\r"; | ||
last = ++i; | ||
} else if (char === 116) { | ||
result += "\t"; | ||
last = ++i; | ||
} else if (char === 117 && load<u64>(changetype<usize>(data) + <usize>((i + 1) << 1)) === 27584753879220272) { | ||
result += "\u000b"; | ||
i += 4; | ||
last = ++i; | ||
} | ||
} | ||
} | ||
} | ||
result += data.slice(last); | ||
// @ts-ignore | ||
return result; | ||
} else if (isBoolean<T>()) { | ||
@@ -200,5 +223,5 @@ // @ts-ignore | ||
// @ts-ignore | ||
@inline | ||
// @ts-ignore | ||
function serializeString(data: string): string { | ||
@@ -210,3 +233,3 @@ // @ts-ignore | ||
if (data.length === 1) { | ||
char === unsafeCharCodeAt(data, 0); | ||
char = unsafeCharCodeAt(data, 0); | ||
if (char === 34) { | ||
@@ -368,2 +391,6 @@ return "\\\""; | ||
export function parseNumber<T>(data: string): T { | ||
if (isInteger<T>()) { | ||
// @ts-ignore | ||
return atoi_fast<T>(data); | ||
} | ||
// @ts-ignore | ||
@@ -375,18 +402,2 @@ const type: T = 0; | ||
else if (type instanceof f32) return f32.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof u64) return u64.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof u32) return u32.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof u8) return u8.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof u16) return u16.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof i64) return i64.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof i32) return i32.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof i16) return i16.parse(data); | ||
// @ts-ignore | ||
else if (type instanceof i8) return i8.parse(data); | ||
} | ||
@@ -393,0 +404,0 @@ |
import { StringSink } from "as-string-sink/assembly"; | ||
import { isSpace } from "util/string"; | ||
import { CharCode, isSpace } from "util/string"; | ||
import { backSlashCode, quoteCode } from "./chars"; | ||
@@ -81,1 +81,22 @@ import { u128, u128Safe, u256, u256Safe, i128, i128Safe, i256Safe } from "as-bignum/assembly"; | ||
} | ||
/** | ||
* Implementation of ATOI. Can be much much faster with SIMD. | ||
* Its pretty fast. (173m ops (atoi_fast) vs 89 ops (parseInt)) | ||
*/ | ||
@unsafe | ||
@inline | ||
export function atoi_fast<T extends number>(str: string): T { | ||
// @ts-ignore | ||
let val: T = 0; | ||
for (let pos = 0; pos < str.length; pos += 2) { | ||
// @ts-ignore | ||
val = (val << 1) + (val << 3) + (load<u16>(changetype<usize>(str) + <usize>pos) - 48); | ||
// We use load because in this case, there is no need to have bounds-checking | ||
} | ||
return val; | ||
} | ||
/** | ||
* | ||
*/ |
@@ -0,2 +1,4 @@ | ||
import { backSlashCode, quoteCode } from "./src/chars"; | ||
import { JSON } from "./src/json"; | ||
import { atoi_fast, unsafeCharCodeAt } from "./src/util"; | ||
@@ -9,31 +11,62 @@ // @json or @serializable work here | ||
z!: f32; | ||
@inline | ||
__JSON_Serialize(): string { | ||
return `{"x":${this.x.toString()},"y":${this.y.toString()},"z":${this.z.toString()}}`; | ||
} | ||
@inline | ||
__JSON_Deserialize(data: string, to: Vec3): Vec3 { | ||
let last = 1; | ||
let char = 0; | ||
let inStr = false; | ||
let key: string | null = null; | ||
let pos = 0; | ||
for (; pos < data.length - 1; pos++) { | ||
char = unsafeCharCodeAt(data, pos); | ||
if (inStr === false && char === quoteCode) { | ||
if (key != null) { | ||
if (key == "x") { | ||
to.x = f32.parse(data.slice(last, pos - 1)) | ||
} else if (key == "y") { | ||
to.y = f32.parse(data.slice(last, pos - 1)) | ||
} else if (key == "z") { | ||
to.z = f32.parse(data.slice(last, pos - 1)) | ||
} | ||
} | ||
last = ++pos; | ||
inStr = true; | ||
} else if (char === quoteCode && unsafeCharCodeAt(data, pos - 1) != backSlashCode) { | ||
inStr = false; | ||
key = data.slice(last, pos); | ||
last = pos += 2; | ||
} | ||
} | ||
if (key != null) { | ||
if (key == "x") { | ||
to.x = f32.parse(data.slice(last, pos - 1)) | ||
} else if (key == "y") { | ||
to.y = f32.parse(data.slice(last, pos - 1)) | ||
} else if (key == "z") { | ||
to.z = f32.parse(data.slice(last, pos - 1)) | ||
} | ||
} | ||
return to; | ||
} | ||
} | ||
@json | ||
class Player { | ||
firstName!: string; | ||
lastName!: string; | ||
lastActive!: i32[]; | ||
age!: i32; | ||
pos!: Vec3 | null; | ||
isVerified!: boolean; | ||
const vec: Vec3 = { | ||
x: 3.4, | ||
y: 1.2, | ||
z: 8.3 | ||
} | ||
const player: Player = { | ||
firstName: "Emmet", | ||
lastName: "West", | ||
lastActive: [8, 27, 2022], | ||
age: 23, | ||
pos: { | ||
x: 3.4, | ||
y: 1.2, | ||
z: 8.3 | ||
}, | ||
isVerified: true | ||
}; | ||
const serializedVec3 = vec.__JSON_Serialize(); | ||
console.log(serializedVec3); | ||
const stringified = JSON.stringify<Player>(player); | ||
console.log(stringified); | ||
const parsedVec3 = vec.__JSON_Deserialize(serializedVec3, new Vec3()); | ||
console.log(parsedVec3.__JSON_Serialize()); | ||
const parsed = JSON.parse<Player>(stringified); | ||
console.log(JSON.stringify(parsed)); | ||
console.log(`atoi_fast("429496729"): ${atoi_fast<i32>("429496729")}`); | ||
console.log(`strtol("429496729"): ${i32.parse("429496729")}`); |
{ | ||
"name": "json-as", | ||
"version": "0.5.31", | ||
"version": "0.5.32", | ||
"description": "JSON encoder/decoder for AssemblyScript", | ||
@@ -15,3 +15,3 @@ "types": "assembly/index.ts", | ||
"aspect": "asp", | ||
"bench:astral": "astral", | ||
"bench:astral": "astral -Ospeed --noAssert --uncheckedBehavior always", | ||
"build:test": "asc assembly/test.ts --target test --runtime stub", | ||
@@ -27,5 +27,5 @@ "build:transform": "tsc -p ./transform", | ||
"@as-tral/cli": "^2.0.0", | ||
"@assemblyscript/loader": "^0.27.0", | ||
"@assemblyscript/loader": "^0.27.1", | ||
"@assemblyscript/wasi-shim": "^0.1.0", | ||
"assemblyscript": "^0.27.0", | ||
"assemblyscript": "^0.27.1", | ||
"assemblyscript-prettier": "^1.0.7", | ||
@@ -32,0 +32,0 @@ "prettier": "^2.8.4", |
@@ -5,3 +5,3 @@ # AS-JSON | ||
Probably the fastest JSON implementation for AssemblyScript with many more optimizations coming down the pipeline. | ||
JSON for AssemblyScript focused on performance, low-overhead, and ease-of-use. | ||
## Installation | ||
@@ -22,3 +22,3 @@ | ||
Add the transform to your `asc` command | ||
Add the transform to your `asc` command (e.g. in package.json) | ||
@@ -29,3 +29,3 @@ ```bash | ||
Or, add it to `asconfig.json` | ||
Alternatively, add it to your `asconfig.json` | ||
@@ -81,6 +81,2 @@ ``` | ||
## Notes | ||
Performance exceeds JavaScript JSON implementation by an average of 230% but this decreases with larger data packets. | ||
## Planned Features | ||
@@ -108,16 +104,24 @@ | ||
**Serialize Object (Vec3):** ~11.1m ops/s | ||
Number parsing speed has doubled over the last 5 versions due to the use of a `atoi_fast` function found in `assembly/util.ts`. This can be further optimized with SIMD. | ||
**Deserialize Object (Vec3):** ~3.2m ops/s | ||
String serialization has more than tripled in performance (+360%). The algorithm was rewritten for less if statements and more traps. | ||
**Serialize Array (int[]):** ~1.4m ops/s | ||
String deserialization was quadrupled from 3.4m ops to 14.8 ops (435%). It went from using `.replaceAll` to a specialized algorithm. | ||
**Deserialize Array (int[]):** ~2.8m ops/s | ||
Schema (object) parsing is being optimized on GitHub and should be at least doubled if not tripled. (Current speed of barely-optimized version is 6.9m ops) This is due to taking advantage of the types with a type map and eliminating extra checking. | ||
**Serialize String (5):** ~4.2m ops/s | ||
**Serialize Object (Vec3):** 6.7m ops/5s | ||
**Deserialize String (5):** ~12m ops/s | ||
**Deserialize Object (Vec3):** 3.8m ops/5s | ||
**Serialize Array (int[]):** 6.6m ops/5s | ||
**Deserialize Array (int[]):** 8.6m ops/5s | ||
**Serialize String (5):** 5.9m ops/5s | ||
**Deserialize String (5):** 16.3m ops/5s | ||
## Issues | ||
Please submit an issue to https://github.com/JairusSW/as-json/issues if you find anything wrong with this library |
@@ -40,2 +40,4 @@ import { getName, toString, isStdlib } from "visitor-as/dist/utils.js"; | ||
} | ||
this.currentClass.keys.push(name); | ||
this.currentClass.types.push(type); | ||
// @ts-ignore | ||
@@ -129,4 +131,2 @@ //this.decodeStmts.push( | ||
this.schemasList.push(this.currentClass); | ||
//console.log(serializeFunc); | ||
//console.log(setKeyFunc); | ||
} | ||
@@ -133,0 +133,0 @@ visitSource(node) { |
{ | ||
"name": "@json-as/transform", | ||
"version": "0.5.31", | ||
"version": "0.5.32", | ||
"description": "JSON encoder/decoder for AssemblyScript", | ||
@@ -5,0 +5,0 @@ "main": "./lib/index.js", |
@@ -47,3 +47,5 @@ import { | ||
} | ||
this.currentClass.keys.push(name); | ||
this.currentClass.types.push(type); | ||
// @ts-ignore | ||
@@ -140,3 +142,3 @@ //this.decodeStmts.push( | ||
` | ||
const serializeMethod = SimpleParser.parseClassMember(serializeFunc, node); | ||
@@ -152,5 +154,2 @@ node.members.push(serializeMethod); | ||
this.schemasList.push(this.currentClass); | ||
//console.log(serializeFunc); | ||
//console.log(setKeyFunc); | ||
} | ||
@@ -157,0 +156,0 @@ visitSource(node: Source): void { |
74967
7.43%24
4.35%1696
6.6%122
3.39%81
1.25%