Skip to content

Commit

Permalink
refactor: remove config of string codec threshold
Browse files Browse the repository at this point in the history
BREAKING CHANGES:

Calls to `TextDecoder.decode`, `TextEncoder.encode` have a fixed cost.
This cost outperforms the native perf to decode/encode small strings.

_bare-ts_ uses a custom implementation to decode and encode small strs.
The choice between custom and native codecs is based on thresholds.
These threshold were configurable via `textDecoderThreshold` and
`textEncoderThreshold` config properties.

This is not clear whether these configurations are worth to expose.
Most of decoded and encoded strings are small.
Fixed thresholds seem fair enough.
  • Loading branch information
Conaclos committed Apr 30, 2023
1 parent a6e8561 commit 53518ed
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 53 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ This project adheres to [Semantic Versioning][semver].
+ bare.readUnsafeU8FixedArray(bc, 5)
```

- BREAKING CHANGES: remove `textDecoderThreshold` and `textEncoderThreshold` configuration

Calls to native `TextDecoder.decode` and `TextEncoder.encode` have a fixed cost.
This cost outperforms the native performance to decode and encode small strings.

_bare-ts_ uses a custom implementation to decode and encode small strings.
The choice between the custom and the native codecs is based on thresholds.
These threshold were configurable via `textDecoderThreshold` and `textEncoderThreshold` config properties.

This is not clear whether this configuration is worth to expose.
Most of decoded and encoded strings are small.
Fixed thresholds seem fair enough.

- Assertions and development mode

Previously, bare-ts enabled a few assertions to check some function arguments.
Expand Down
12 changes: 8 additions & 4 deletions src/codec/string.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { BareError } from "../core/bare-error.js"
import { type ByteCursor, check, reserve } from "../core/byte-cursor.js"
import { DEV, assert } from "../util/assert.js"
import { INVALID_UTF8_STRING } from "../util/constants.js"
import {
INVALID_UTF8_STRING,
TEXT_DECODER_THRESHOLD,
TEXT_ENCODER_THRESHOLD,
} from "../util/constants.js"
import { isU32 } from "../util/validator.js"
import { readUintSafe32, writeUintSafe32 } from "./primitive.js"
import { readUnsafeU8FixedArray, writeU8FixedArray } from "./u8-array.js"
Expand All @@ -11,7 +15,7 @@ export function readString(bc: ByteCursor): string {
}

export function writeString(bc: ByteCursor, x: string): void {
if (x.length < bc.config.textEncoderThreshold) {
if (x.length < TEXT_ENCODER_THRESHOLD) {
const byteLen = utf8ByteLength(x)
writeUintSafe32(bc, byteLen)
reserve(bc, byteLen)
Expand All @@ -27,7 +31,7 @@ export function readFixedString(bc: ByteCursor, byteLen: number): string {
if (DEV) {
assert(isU32(byteLen))
}
if (byteLen < bc.config.textDecoderThreshold) {
if (byteLen < TEXT_DECODER_THRESHOLD) {
return readUtf8Js(bc, byteLen)
}
try {
Expand All @@ -38,7 +42,7 @@ export function readFixedString(bc: ByteCursor, byteLen: number): string {
}

export function writeFixedString(bc: ByteCursor, x: string): void {
if (x.length < bc.config.textEncoderThreshold) {
if (x.length < TEXT_ENCODER_THRESHOLD) {
const byteLen = utf8ByteLength(x)
reserve(bc, byteLen)
writeUtf8Js(bc, x)
Expand Down
8 changes: 0 additions & 8 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,15 @@ import { isU32 } from "../util/validator.js"
export type Config = {
readonly initialBufferLength: number
readonly maxBufferLength: number
readonly textDecoderThreshold: number
readonly textEncoderThreshold: number
}

export function Config({
initialBufferLength = 1024,
maxBufferLength = 1024 * 1024 * 32 /* 32 MiB */,
textDecoderThreshold = 256,
textEncoderThreshold = 256,
}): Config {
if (DEV) {
assert(isU32(initialBufferLength), TOO_LARGE_NUMBER)
assert(isU32(maxBufferLength), TOO_LARGE_NUMBER)
assert(isU32(textDecoderThreshold), TOO_LARGE_NUMBER)
assert(isU32(textEncoderThreshold), TOO_LARGE_NUMBER)
assert(
initialBufferLength <= maxBufferLength,
"initialBufferLength must be lower than or equal to maxBufferLength",
Expand All @@ -28,7 +22,5 @@ export function Config({
return {
initialBufferLength,
maxBufferLength,
textDecoderThreshold,
textEncoderThreshold,
}
}
3 changes: 3 additions & 0 deletions src/util/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export const TEXT_DECODER_THRESHOLD = 256
export const TEXT_ENCODER_THRESHOLD = 256

export const INT_SAFE_MAX_BYTE_COUNT = 8
export const UINT_MAX_BYTE_COUNT = 10
export const UINT_SAFE32_MAX_BYTE_COUNT = 5
Expand Down
7 changes: 1 addition & 6 deletions tests/codec/_util.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import { ByteCursor, Config } from "@bare-ts/lib"

export function fromConfigBytes(
partConfig: Partial<Config>,
...rest: number[]
): ByteCursor
import { ByteCursor } from "@bare-ts/lib"

export function fromBytes(...rest: number[]): ByteCursor

Expand Down
10 changes: 0 additions & 10 deletions tests/codec/_util.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
import { ByteCursor, Config } from "@bare-ts/lib"

/**
*
* @param {Partial<Config>} partConfig
* @param {...number[]} rest
* @returns {ByteCursor}
*/
export function fromConfigBytes(partConfig, ...rest) {
return new ByteCursor(Uint8Array.from(rest), Config(partConfig))
}

/**
*
* @param {...number[]} rest
Expand Down
56 changes: 31 additions & 25 deletions tests/codec/string.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import * as bare from "@bare-ts/lib"
import { default as test } from "oletus"

import { fromBytes, fromConfigBytes, toBytes } from "./_util.js"
import { fromBytes, toBytes } from "./_util.js"

test("bare.readString", (t) => {
let bc = fromConfigBytes(
{ textDecoderThreshold: 0 },
/* byteLength */ 4,
98,
97,
114,
101,
test("bare.readFixedString", (t) => {
let bc = fromBytes(
..."b"
.repeat(300)
.split("")
.map((s) => s.charCodeAt(0)),
)
t.deepEqual(
bare.readFixedString(bc, 300),
"b".repeat(300),
"can natively read ASCII",
)
t.deepEqual(bare.readString(bc), "bare", "can natively read ASCII")

bc = fromBytes(/* byteLength */ 4, 98, 97, 114, 101)
bc = fromBytes(98, 97, 114, 101)
t.deepEqual(bare.readFixedString(bc, 4), "bare", "can read ASCII")
})

test("bare.readString", (t) => {
let bc = fromBytes(/* byteLength */ 4, 98, 97, 114, 101)
t.deepEqual(bare.readString(bc), "bare", "can read ASCII")

bc = fromBytes(/* byteLength */ 6, 0xc3, 0xa9, 0xc3, 0xa0, 0xc3, 0xb9)
Expand Down Expand Up @@ -167,16 +174,25 @@ test("bare.readString", (t) => {
)
})

test("bare.writeString", (t) => {
let bc = fromConfigBytes({ textEncoderThreshold: 0 })
bare.writeString(bc, "bare")
test("bare.writeFixedString", (t) => {
let bc = fromBytes()
bare.writeFixedString(bc, "b".repeat(300))
t.deepEqual(
toBytes(bc),
[/* byteLength */ 4, 98, 97, 114, 101],
"b"
.repeat(300)
.split("")
.map((s) => s.charCodeAt(0)),
"can natively write ASCII",
)

bc = fromBytes()
bare.writeFixedString(bc, "bare")
t.deepEqual(toBytes(bc), [98, 97, 114, 101], "can write ASCII")
})

test("bare.writeString", (t) => {
let bc = fromBytes()
bare.writeString(bc, "bare")
t.deepEqual(
toBytes(bc),
Expand Down Expand Up @@ -270,13 +286,3 @@ test("bare.writeString", (t) => {
],
)
})

test("bare.writeFixedString", (t) => {
let bc = fromConfigBytes({ textEncoderThreshold: 0 })
bare.writeFixedString(bc, "bare")
t.deepEqual(toBytes(bc), [98, 97, 114, 101], "can natively write ASCII")

bc = fromBytes()
bare.writeFixedString(bc, "bare")
t.deepEqual(toBytes(bc), [98, 97, 114, 101], "can write ASCII")
})

0 comments on commit 53518ed

Please sign in to comment.