A fast timestamp parser in Swift

I wrote a timestamp parser in Swift. It's called Parse3339.

It's well known that DateFormatter, the main timestamp formatter and parser Apple ships in Foundation, is not particularly fast. It's flexible and it's correct, but it takes its time. The newer ISO8601DateFormatter has similar performance.

I haven't much worried about that in the recent years. A while ago I had a problem with slow parsing but that time a caching layer solved it, as the data had a ton of identical dates. Last week I saw date parsers rear their heads in an Instruments trace again, and this time the data wasn't amenable to caching. It was happening on a background queue and it was only a few hundred milliseconds. But once you start optimizing, it's hard to let go.

In the past a common solution was using strptime(3). This time I wanted to see how far I could get with Swift. I've usually used parser combinators for parsing, but these time stamps are so regular a simpler approach felt sufficient.

Time formats have two relevant standards: ISO 8601 and RFC 3339. RFC 3339 is mostly a subset of ISO 8601, but even it allows for all kinds of irrelevant silliness. Realistically the only format I care about is a full timestamp with a T between date and time and possibly a Z for time zone. When I don't support any other variations, the resulting parser is really small, straightforward and easy to modify if your needs are different.

It's also very fast. The parser itself easily breezes through hundreds of thousands timestamps per second on my Mac. However, in most cases, you need something other than a struct with a bunch of numbers. Things slowed down once I started creating DateComponents and converting those to Dates with a Calendar. The code was still a few times faster than the platform parsers, but it was clearly leaving performance on the table. It was also using a lot of memory even though the parser itself was completely running on stack, allocating nothing. Benchmarking revealed that my code was using a similar amount of memory as DateFormatter, whereas ISO8601DateFormatter had a lighter footprint.

So back to the old Unix functions. Swift's C bridging is first-class; struct tm and timegm(3) are right there. After changing the code to use those, the whole String to Date conversion runs completely without heap allocations and it's around 6–7 times faster than when doing the detour via DateComponents.

The final result, according to Benchmark, is maybe around 15x the speed of the Foundation parsers. I'm pretty happy with it. It's available as a Swift package, there's documentation, and if you need something different or don't feel like using a package, all the parsing code is in just one file you can copy over to your project.

Git worktrees helper

I recently became an avid user of Git worktrees. However, the command line interface to them is about as great as Git command line interfaces always are. Just git worktree add is a confusing maze of options.

I wrote a shell script to help myself and then I wanted to check parameters and then I decided that was a bridge too far with shell today and went for Swift instead.

My Worktrees tool has just one command for now, because that's the pain point I ran into today. worktrees add-new-branch username/ mybranch main creates a new branch called username/mybranch and a worktree called mybranch for working on it, branching it off main. You could very well argue that it's no easier than git worktree add, but the idea is that I can define a repo-local alias for it. Almost all the branches I open are from the mainline branch of the repo I'm working in, and I want to prefix my branches with juri/. So I can add this alias, if the repo is using main:

[alias]
	wta = !worktree add-new-branch juri/ main

And now running git wta mybranch does opens me a new worktree the way I like it.

Better diff hunk headers with Swift

When you run git diff — or look at diffs in at least Fork — on a modified Swift project you see things like this:

@@ -251,7 +251,7 @@ extension AppUITests {
         let container = app.scrollViews["scroll"]
         XCTAssertTrue(container.waitForExistence(timeout: 2))
-        XCTAssertTrue(container.buttons["Restore"].exists)
+        XCTAssertTrue(container.buttons["Restore Purchases"].exists)
         XCTAssertTrue(container.buttons["View Subscriptions"].exists)
     }

While staring at a bunch of these today I realized just how unhelpful that line with @@s and extension is. The name of the extension where these lines live is not the most relevant context; the name of the function is.

This article on improving diff output for a bunch of other languages was helpful. I'll adapt its guidance for Swift here.

Step 1: Diff driver

Define a diff driver in your $HOME/.gitconfig. The xfuncname configuration specifies a regular expression that is used to match a line you want to see in the hunk header after the @@ bit. Covering all possible options with a regexp probably isn't possible, but this should cover most of the cases:

[diff "swift"]
    xfuncname = ^[ \t]*(((private |public |internal |final |open )*class|(private |public |internal )*struct|(private |public |internal )*actor|(private |public |internal )*func|(private |public |internal )*extension|(private |public |internal )*enum)[ \t].*)$

Step 2: Global git attributes

If you don't have a global git attributes file configured, set one up:

git config --global core.attributesfile ~/.gitattributes

Step 3: Configure the swift driver for Swift files

Edit the ~/.gitattributes file to make Git use your newly defined diff driver for Swift files. Add the following line:

*.swift diff=swift

After these changes, the above diff will look like this:

@@ -251,7 +251,7 @@ func testButtons() {
         let container = app.scrollViews["scroll"]
         XCTAssertTrue(container.waitForExistence(timeout: 2))
-        XCTAssertTrue(container.buttons["Restore"].exists)
+        XCTAssertTrue(container.buttons["Restore Purchases"].exists)
         XCTAssertTrue(container.buttons["View Subscriptions"].exists)
     }

That's a lot more helpful.

© Juri Pakaste 2024