Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.0.5 proposal #9

Merged
merged 5 commits into from Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "pprof-format",
"version": "2.0.4",
"version": "2.0.5",
"description": "Pure JavaScript pprof encoder and decoder",
"author": "Datadog Inc. <info@datadoghq.com>",
"license": "MIT",
Expand Down
63 changes: 53 additions & 10 deletions src/index.test.ts
Expand Up @@ -16,7 +16,8 @@ import {
Profile,
Sample,
ValueType,
StringTable
StringTable,
emptyTableToken
} from './index.js'

type Data = {
Expand Down Expand Up @@ -56,7 +57,11 @@ tap.Test.prototype.addAssert('encodes', 3, function (Type: any, data: Data, enco
return this.test(message, (t: TestSuite) => {
t.test('per-field validation', (t2: TestSuite) => {
for (const { field, value } of encodings) {
const fun = new Type({ [field]: data[field] })
const fun = new Type({
// Hack to exclude stringTable data from any checks except for the string table itself
stringTable: new StringTable(emptyTableToken),
[field]: data[field]
})
const msg = `has expected encoding of ${field} field`
t2.equal(bufToHex(fun.encode()), value, msg)
}
Expand Down Expand Up @@ -268,7 +273,8 @@ const profileData = {
periodType: valueTypeData,
period: 1234 / 2,
comment: [
stringTable.dedup('some comment')
stringTable.dedup('some very very very very very very very very very very very very very very very very very very very very very very very very comment'),
stringTable.dedup('another comment')
]
}

Expand All @@ -283,7 +289,7 @@ const profileEncodings = [
{ field: 'durationNanos', value: '50d209' },
{ field: 'periodType', value: embeddedField('5a', valueTypeEncodings) },
{ field: 'period', value: '60e904' },
{ field: 'comment', value: '6a010b' },
{ field: 'comment', value: '6a020b0c' },
]

tap.test('Profile', (t: TestSuite) => {
Expand All @@ -294,21 +300,42 @@ tap.test('Profile', (t: TestSuite) => {
})

function encodeStringTable(strings: StringTable) {
return strings.slice(1).map(s => {
return strings.strings.map(s => {
const buf = new TextEncoder().encode(s)
return `32${hexNum(buf.length)}${bufToHex(buf)}`
return `32${hexVarInt(buf.length)}${bufToHex(buf)}`
}).join('')
}

function hexNum(num: number) {
let str = num.toString(16)
if (str.length % 2) str = '0' + str
function hexNum(d: number) {
let hex = Number(d).toString(16);

if (hex.length == 1) {
hex = "0" + hex;
}

return hex;
}

function hexVarInt(num: number) {
let n = BigInt(num)
if (n < 0) {
// take two's complement to encode negative number
n = 2n ** 64n - n
}
let str = ''
const maxbits = 7n
const max = (1n << maxbits) - 1n
while (n > max) {
str += hexNum(Number((n & max) | (1n << maxbits)))
n >>= maxbits
}
str += hexNum(Number(n))
return str
}

function embeddedField(fieldBit: string, data: Encoding[]) {
const encoded = fullEncoding(data)
const size = hexNum(encoded.length / 2)
const size = hexVarInt(encoded.length / 2)
return [fieldBit, size, encoded].join('')
}

Expand All @@ -323,3 +350,19 @@ function hexToBuf(hex: string) {
function bufToHex(buf: Uint8Array) {
return Array.from(buf).map(hexNum).join('')
}

tap.test('StringTable', (t: TestSuite) => {
t.test('encodes correctly', (t: TestSuite) => {
const encodings = {
'': '3200',
'hello': '320568656c6c6f'
}
const table = new StringTable()
t.equal(bufToHex(table.encode()), encodings[''])
table.dedup('hello')
t.equal(bufToHex(table.encode()), encodings[''] + encodings['hello'])
t.end()
})

t.end()
})
80 changes: 43 additions & 37 deletions src/index.ts
Expand Up @@ -55,7 +55,7 @@ function getValue(mode: number, buffer: Uint8Array) {
case kTypeLengthDelim: {
const offset = countNumberBytes(buffer)
const size = decodeNumber(buffer)
return makeValue(buffer.slice(offset, Number(size) + 1), offset)
return makeValue(buffer.slice(offset, Number(size) + offset), offset)
}
default:
throw new Error(`Unrecognized value type: ${mode}`)
Expand Down Expand Up @@ -222,68 +222,72 @@ function encodeNumber(buffer: Uint8Array, i: number, number: Numeric): number {
return i
}

export class StringTable extends Array {
#encodings = new Map<string, Uint8Array>()
#positions = new Map<string, number>()

constructor() {
super()
this.push('')
}
export const emptyTableToken = Symbol()

static from(values: StringTable | Array<string>): StringTable {
if (values instanceof StringTable) {
return values
}
export class StringTable {
strings = new Array<string>()
#encodings = new Array<Uint8Array>()
#positions = new Map<string, number>()

// Need to copy over manually to ensure the lookup map is correct
const table = new StringTable()
for (const value of values) {
table.#positions.set(value, table.push(value) - 1)
table.#encodings.set(value, StringTable._encodeString(value))
constructor(tok?: typeof emptyTableToken) {
if (tok !== emptyTableToken) {
this.dedup('')
}

return table
}

get encodedLength(): number {
let size = 0
for (const encoded of this.#encodings.values()) {
for (const encoded of this.#encodings) {
size += encoded.length
}
return size
}

encode(buffer: Uint8Array, offset: number): number {
for (const encoded of this.#encodings.values()) {
_encodeToBuffer(buffer: Uint8Array, offset: number): number {
for (const encoded of this.#encodings) {
buffer.set(encoded, offset)
offset += encoded.length
}
return offset
}

static _encodeString(string: string): Uint8Array {
const stringBuffer = toUtf8(string)
const buffer = new Uint8Array(1 + stringBuffer.length + measureNumber(stringBuffer.length))
encode(buffer = new Uint8Array(this.encodedLength)): Uint8Array {
this._encodeToBuffer(buffer, 0)
return buffer
}

static _encodeStringFromUtf8(stringBuffer: Uint8Array | Buffer): Uint8Array {
const buffer = new Uint8Array(1 + stringBuffer.length + (measureNumber(stringBuffer.length) || 1))
let offset = 0
buffer[offset++] = 50 // (6 << 3) + kTypeLengthDelim
offset = encodeNumber(buffer, offset, stringBuffer.length)
buffer.set(stringBuffer, offset++)
if (stringBuffer.length > 0) {
buffer.set(stringBuffer, offset++)
}
return buffer
}

static _encodeString(string: string): Uint8Array {
return StringTable._encodeStringFromUtf8(toUtf8(string))
}

dedup(string: string): number {
if (!string) return 0
if (typeof string === 'number') return string
if (!this.#positions.has(string)) {
const pos = this.push(string) - 1
const pos = this.strings.push(string) - 1
this.#positions.set(string, pos)

// Encode strings on insertion
this.#encodings.set(string, StringTable._encodeString(string))
this.#encodings.push(StringTable._encodeString(string))
}
return this.#positions.get(string)
}

_decodeString(buffer: Uint8Array) {
const string = new TextDecoder().decode(buffer)
this.#positions.set(string, this.strings.push(string) - 1)
this.#encodings.push(StringTable._encodeStringFromUtf8(buffer))
}
}

function decode<T>(
Expand Down Expand Up @@ -899,7 +903,7 @@ export type ProfileInput = {
mapping?: Array<MappingInput>
location?: Array<LocationInput>
function?: Array<FunctionInput>
stringTable?: StringTable | string[]
stringTable?: StringTable
dropFrames?: Numeric
keepFrames?: Numeric
timeNanos?: Numeric
Expand Down Expand Up @@ -932,7 +936,7 @@ export class Profile {
this.mapping = (data.mapping || []).map(v => new Mapping(v))
this.location = (data.location || []).map(v => new Location(v))
this.function = (data.function || []).map(v => new Function(v))
this.stringTable = StringTable.from(data.stringTable || [])
this.stringTable = data.stringTable || new StringTable()
this.dropFrames = data.dropFrames || 0
this.keepFrames = data.keepFrames || 0
this.timeNanos = data.timeNanos || 0
Expand All @@ -957,7 +961,7 @@ export class Profile {
total += measureNumberField(this.durationNanos)
total += measureLengthDelimField(this.periodType)
total += measureNumberField(this.period)
total += measureLengthDelimArrayField(this.comment)
total += measureNumberArrayField(this.comment)
total += measureNumberField(this.defaultSampleType)
return total
}
Expand Down Expand Up @@ -993,7 +997,7 @@ export class Profile {
offset = fun._encodeToBuffer(buffer, offset)
}

offset = this.stringTable.encode(buffer, offset)
offset = this.stringTable._encodeToBuffer(buffer, offset)

if (this.dropFrames) {
buffer[offset++] = 56 // (7 << 3) + kTypeVarInt
Expand Down Expand Up @@ -1065,8 +1069,10 @@ export class Profile {
data.function = push(Function.decode(buffer), data.function)
break
case 6: {
const string = new TextDecoder().decode(buffer)
data.stringTable = StringTable.from(push(string, data.stringTable as StringTable))
if (data.stringTable === undefined) {
data.stringTable = new StringTable(emptyTableToken)
}
data.stringTable._decodeString(buffer)
break
}
case 7:
Expand All @@ -1088,7 +1094,7 @@ export class Profile {
data.period = decodeNumber(buffer)
break
case 13:
data.comment = push(decodeNumber(buffer), data.comment)
data.comment = decodeNumbers(buffer)
break
case 14:
data.defaultSampleType = decodeNumber(buffer)
Expand Down