Skip to content

Commit b2655ed

Browse files
weissiJohannes Weiss
andauthored
improve sample conversion speed (#33)
### Motivation: #31 showed that we spend a lot of time parsing the JSON for `.swipr` raw sample files. In particular, parsing lines like this ``` [SWIPR] STCK {"ip": "0x18fe24c08", "sp": "0x1702a6fe0"} ``` into `StackFrame(instructionPointer: 0x18fe24c08, stackPointer: whateverWeIgnoreItAnyway)` took over 50% of the time in my testing. ### Modification Write a very bad but relatively fast hand-rolled JSON parser to make this a lot faster. ### Result - fixes #31 Before: ``` $ time .build/release/swipr-sample-conv --use-fake-symbolizer true /tmp/huge.swipr > /tmp/hugefake.perf 2025-10-07T20:51:21+0100 info swipr-sample-conv: cached-sym=CachedSymbolizer(cache: 503, vmaps: 355, sym: FakeSymbolizer) [ProfileRecorderSampleConversion] done symbolising real 0m23.742s user 0m23.080s sys 0m0.617s ``` After: ``` $ time .build/release/swipr-sample-conv --use-fake-symbolizer true /tmp/huge.swipr > /tmp/hugefake.perf 2025-10-07T21:44:15+0100 info swipr-sample-conv: cached-sym=CachedSymbolizer(cache: 503, vmaps: 355, sym: FakeSymbolizer) [ProfileRecorderSampleConversion] done symbolising real 0m9.394s user 0m8.804s sys 0m0.543s ``` -> more than 2x faster Co-authored-by: Johannes Weiss <[email protected]>
1 parent 54be04e commit b2655ed

File tree

3 files changed

+184
-2
lines changed

3 files changed

+184
-2
lines changed

Sources/ProfileRecorderSampleConversion/HelperFunctions.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,64 @@ import Foundation
2020
func swift_reportWarning(_ dunno: Int, _ message: String) {
2121
fputs("WARNING: \(message)\n", stderr)
2222
}
23+
24+
extension Substring.UTF8View {
25+
/// This is a very terrible JSON parser that should just about be able to parse a `StackFrame`
26+
///
27+
/// - note: We are ignoring the stack pointer, so the stack pointer field will always be 0.
28+
public func attemptFastParseStackFrame() -> StackFrame? {
29+
enum State {
30+
case beginning
31+
case ipWaitingFor0x
32+
case ipStartRange(String.Index, String.Index?)
33+
34+
var isWaitingFor0x: Bool {
35+
if case .ipWaitingFor0x = self {
36+
return true
37+
}
38+
return false
39+
}
40+
}
41+
var state: State = .beginning
42+
let bytes = self
43+
loop: for byteIndex in bytes.indices {
44+
let byte = bytes[byteIndex]
45+
switch byte {
46+
case UInt8(ascii: "\""):
47+
if case .ipStartRange(let start, .none) = state {
48+
state = .ipStartRange(start, byteIndex)
49+
break loop
50+
}
51+
continue
52+
case UInt8(ascii: " "), UInt8(ascii: ":"), UInt8(ascii: "{"), UInt8(ascii: "}"),
53+
UInt8(ascii: "p"):
54+
// example line:
55+
// {"ip": "0x18fe24c08", "sp": "0x1702a6fe0"}
56+
// we ignore everything that's not an 'i', an 's' or hex digit like
57+
// __i_____0x18fe24c08____s_____0x1702a6fe0
58+
continue
59+
case UInt8(ascii: "i"):
60+
state = .ipWaitingFor0x
61+
case UInt8(ascii: "0")...UInt8(ascii: "9"),
62+
UInt8(ascii: "a")...UInt8(ascii: "f"),
63+
UInt8(ascii: "A")...UInt8(ascii: "F"):
64+
// ignore
65+
continue
66+
case UInt8(ascii: "x") where state.isWaitingFor0x:
67+
state = .ipStartRange(bytes.index(after: byteIndex), nil)
68+
break
69+
default:
70+
continue
71+
}
72+
}
73+
74+
if case .ipStartRange(let start, .some(let end)) = state {
75+
guard let string = String(self[start..<end]), let ip = UInt(string, radix: 16) else {
76+
return nil
77+
}
78+
return StackFrame(instructionPointer: ip, stackPointer: 0)
79+
} else {
80+
return nil
81+
}
82+
}
83+
}

Sources/ProfileRecorderSampleConversion/ProfileRecorderSampleConverter.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public struct ProfileRecorderSampleConverter: Sendable {
206206

207207
while getline(&buffer, &bufferCapacity, input) != -1 {
208208
let line = String(cString: buffer!)
209-
guard line.starts(with: messageHeaderPrefix) else {
209+
guard line.hasPrefix(messageHeaderPrefix) else {
210210
continue
211211
}
212212
switch line
@@ -298,10 +298,17 @@ public struct ProfileRecorderSampleConverter: Sendable {
298298

299299
currentSample = Sample(sampleHeader: header, stack: [])
300300
case "STCK":
301+
let json = line.dropFirst(messageHeaderLength).utf8
302+
303+
// attempt the fast parser
304+
if let stackFrame = json.attemptFastParseStackFrame() {
305+
currentSample?.stack.append(stackFrame)
306+
continue
307+
}
301308
guard
302309
let stackFrame = try? decoder.decode(
303310
StackFrame.self,
304-
from: Data(line.dropFirst(messageHeaderLength).utf8)
311+
from: Data(json)
305312
)
306313
else {
307314
continue
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Profile Recorder open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift Profile Recorder project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift Profile Recorder project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import XCTest
16+
import ProfileRecorderSampleConversion
17+
18+
final class SampleConversionTests: XCTestCase {
19+
func testBasicExample() {
20+
let line = #"{"ip": "0x18fe24c08", "sp": "0x1702a6fe0"}"#[...]
21+
let actual = line.utf8.attemptFastParseStackFrame()
22+
XCTAssertEqual(StackFrame(instructionPointer: 0x1_8fe2_4c08, stackPointer: 0), actual)
23+
}
24+
25+
func testZero() {
26+
let line = #"{"ip": "0x0", "sp": "0x1702a6fe0"}"#[...]
27+
let actual = line.utf8.attemptFastParseStackFrame()
28+
XCTAssertEqual(StackFrame(instructionPointer: 0x0, stackPointer: 0), actual)
29+
}
30+
31+
func testNoSpaces() {
32+
let line = #"{"ip":"0x18fe24c08","sp":"0x1702a6fe0"}"#[...]
33+
let actual = line.utf8.attemptFastParseStackFrame()
34+
XCTAssertEqual(StackFrame(instructionPointer: 0x1_8fe2_4c08, stackPointer: 0), actual)
35+
}
36+
37+
func testOrder() {
38+
let line = #"{"sp":"0x1702a6fe0","ip":"0x18fe24c08"}"#[...]
39+
let actual = line.utf8.attemptFastParseStackFrame()
40+
XCTAssertEqual(StackFrame(instructionPointer: 0x1_8fe2_4c08, stackPointer: 0), actual)
41+
}
42+
43+
func testExtraFieldsOrder() {
44+
let line = #"{"sp":"0x1702a6fe0","unknown-field":"hello","ip":"0x18fe24c08"}"#[...]
45+
let actual = line.utf8.attemptFastParseStackFrame()
46+
XCTAssertEqual(StackFrame(instructionPointer: 0x1_8fe2_4c08, stackPointer: 0), actual)
47+
}
48+
49+
func testSimpleCase() {
50+
let line = #"{"ip":"0x18fe24c08","sp":"0x1702a6fe0"}"#[...]
51+
let actual = line.utf8.attemptFastParseStackFrame()
52+
XCTAssertEqual(StackFrame(instructionPointer: 0x1_8fe2_4c08, stackPointer: 0), actual)
53+
}
54+
55+
func testIpFirst() {
56+
let line = #"{"ip":"0xABCDEF","sp":"0x1702a6fe0"}"#[...]
57+
let actual = line.utf8.attemptFastParseStackFrame()
58+
XCTAssertEqual(StackFrame(instructionPointer: 0xABCDEF, stackPointer: 0), actual)
59+
}
60+
61+
func testIpLast() {
62+
let line = #"{"sp":"0x1234","thread":"42","flags":"0x9","ip":"0xCAFEBABE"}"#[...]
63+
let actual = line.utf8.attemptFastParseStackFrame()
64+
XCTAssertEqual(StackFrame(instructionPointer: 0xCAFE_BABE, stackPointer: 0), actual)
65+
}
66+
67+
func testIpWithSpaces() {
68+
let line = #" { "ip" : "0xDEAD10CC" , "sp":"0x1702a6fe0" } "#[...]
69+
let actual = line.utf8.attemptFastParseStackFrame()
70+
XCTAssertEqual(StackFrame(instructionPointer: 0xDEAD_10CC, stackPointer: 0), actual)
71+
}
72+
73+
func testIpMixedFields() {
74+
let line = #"{"foo":1,"bar":true,"ip":"0x12345678","baz":[1,2,3]}"#[...]
75+
let actual = line.utf8.attemptFastParseStackFrame()
76+
XCTAssertEqual(StackFrame(instructionPointer: 0x1234_5678, stackPointer: 0), actual)
77+
}
78+
79+
func testIpEmbeddedInText() {
80+
let line = #"{"message":"before","ip":"0xFEEDFACE","after":"something"}"#[...]
81+
let actual = line.utf8.attemptFastParseStackFrame()
82+
XCTAssertEqual(StackFrame(instructionPointer: 0xFEED_FACE, stackPointer: 0), actual)
83+
}
84+
85+
func testIpUpperLowerMix() {
86+
let line = #"{"ip":"0xDeadBeef"}"#[...]
87+
let actual = line.utf8.attemptFastParseStackFrame()
88+
XCTAssertEqual(StackFrame(instructionPointer: 0xDEAD_BEEF, stackPointer: 0), actual)
89+
}
90+
91+
func testIpOnly() {
92+
let line = #"{"ip":"0x11111111"}"#[...]
93+
let actual = line.utf8.attemptFastParseStackFrame()
94+
XCTAssertEqual(StackFrame(instructionPointer: 0x1111_1111, stackPointer: 0), actual)
95+
}
96+
97+
func testIPWithEscapedString() {
98+
let line = #"{"comment":"the ip is \"secret\"","ip":"0xFACEB00C"}"#[...]
99+
let actual = line.utf8.attemptFastParseStackFrame()
100+
XCTAssertEqual(StackFrame(instructionPointer: 0xFACE_B00C, stackPointer: 0), actual)
101+
}
102+
103+
func testNoIPAtAll() {
104+
let line = #"{"comment":"the ip is \"secret\""}"#[...]
105+
let actual = line.utf8.attemptFastParseStackFrame()
106+
XCTAssertEqual(nil, actual)
107+
}
108+
109+
func testPartialIP() {
110+
let line = #"{"comment":"the ip is \"secret\"", "ip": "0x123"#[...]
111+
let actual = line.utf8.attemptFastParseStackFrame()
112+
XCTAssertEqual(nil, actual)
113+
}
114+
}

0 commit comments

Comments
 (0)