More from Krzysztof Kowalczyk blog
New in the SumatraPDF pre-release builds: DDE commands accept arguments Commands sent via DDE can take arguments, the same as in custom shortcuts (#5383). Loading message in tab While a document loads, its tab shows a “loading” message instead of the home page (#5385). Install 32-bit on 64-bit Windows The installer lets you install the 32-bit version on 64-bit Windows (#5379). Changes for this day · Full changelog
I maintain SumatraPDF on GitHub. People fork it and make their own changes. I want to know which forks are active and what they’re working on. GitHub has a Network tab for this. I find it lousy. It’s hard to see active forks at a glance. Panning and zooming the UI is slow and fiddly. I just want a sorted list of forks with actual changes. So I wrote a small script: github-active-forks.ts. What it does It uses the GitHub API to find forks that have ahead commits (changes not in upstream) from the last year. For each active branch it prints: a link to the branch on GitHub short sha, author, relative date, first line of commit message Forks are sorted by most recent activity, oldest first. Commits within a branch are oldest first. It compares each fork branch against the matching upstream branch (e.g. rel3.6working vs rel3.6working), not always master. That way you only see commits unique to the fork. How to run it Download the script: curl -O https://gist.githubusercontent.com/kjk/71a7679dd408d52bc612cdf5eecace58/raw/7d7f605dde6b739424e5190a79e3988477312e74/github-active-forks.ts You need Bun and the GitHub CLI logged in (gh auth login). Or set GITHUB_TOKEN. Run it on any repo as owner/repo: bun github-active-forks.ts sumatrapdfreader/sumatrapdf Progress goes to stderr; results go to stdout, so you can redirect to a file: bun github-active-forks.ts sumatrapdfreader/sumatrapdf > forks.txt Example output Excerpt from running on SumatraPDF (74 forks with ahead commits): https://github.com/wackget/sumatrapdf-Feldherren-version/tree/single-page-fit-scrollbar (289d ago) c53a51d wackget 294d ago Added scrollbar usable in Fit a Single Page display mode. f927640 wackget 294d ago scrollbar in single page mode now obeys the hideScrollbars setting. 7f57861 wackget 288d ago improved scrolling speed when zoomed in, in non-continuous single-page view. https://github.com/xyzzyx99/sumatrapdf/tree/rel3.6working (5d ago) c8d1f7b xyzzyx99 13d ago Add GitHub Actions workflow for building project 8a92547 xyzzyx99 13d ago Fix bugs, revert using version 3.6 1f27324 xyzzyx99 13d ago Save and restore CHM scroll position 24e7974 xyzzyx99 6d ago Preserve current CHM URL across tab restore 30b1cb4 xyzzyx99 5d ago Update README with new bug fixes and formatting https://github.com/lsq/sumatrapdf/tree/dev (today) 72d7069 lsq 11d ago feat: support share file via socket a1fc427 lsq 10d ago feat: open home page 938bc07 lsq today add localsend v2.1 api support Much easier to scan than the Network graph.
A new JavaScript library pretext for fast text measuring / layout popped up on social media. Potentially interesting given its focus on speeding up text rendering in web apps and me writing web apps and liking them being fast. I looked at the code and saw a function isCJK(). Given my 3 decades of programming and performance optimization, it looked like it could be sped up. This is a story about ideas on making JavaScript faster and the process of quickly implementing and benchmarking them. The code export function isCJK(s: string): boolean { for (const ch of s) { const c = ch.codePointAt(0)! if ((c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3400 && c <= 0x4DBF) || (c >= 0x20000 && c <= 0x2A6DF) || (c >= 0x2A700 && c <= 0x2B73F) || (c >= 0x2B740 && c <= 0x2B81F) || (c >= 0x2B820 && c <= 0x2CEAF) || (c >= 0x2CEB0 && c <= 0x2EBEF) || (c >= 0x30000 && c <= 0x3134F) || (c >= 0xF900 && c <= 0xFAFF) || (c >= 0x2F800 && c <= 0x2FA1F) || (c >= 0x3000 && c <= 0x303F) || (c >= 0x3040 && c <= 0x309F) || (c >= 0x30A0 && c <= 0x30FF) || (c >= 0xAC00 && c <= 0xD7AF) || (c >= 0xFF00 && c <= 0xFFEF)) { return true } } return false } My spider sense tingling To make code run fast you have to have mechanical sympathy. You need a good mental model of how CPUs and programming languages work, at the low level. Because I have mechanical sympathy, I know that to evaluate multiple || statements, the program has to check every statement until it finds one that is true. For the case of not matching any range, it has to do all 15 comparisons. My immediate thought was that most characters are ascii (non-cjk) and therefore require 15 comparisons. An early exit should speed things up: if (c < 0x300) continue;. It adds 1 comparison for matching cases but removes 14 (15 - 1) for the most common case of ascii characters. Hunch is good, benchmark is better Micro-benchmarking is subtle. In the past I would google for a JavaScript benchmarking library and trust that it’s sound. Claude wrote a sound, simple benchmark function: function bench(name, fn, iterations = 1_000_000) { for (let i = 0; i < 10_000; i++) fn(); // warmup const start = performance.now(); for (let i = 0; i < iterations; i++) fn(); const elapsed = performance.now() - start; const opsPerSec = ((iterations / elapsed) * 1000).toFixed(0); console.log(`${name}: ${elapsed.toFixed(2)} ms (${opsPerSec} ops/sec)`); } // to call: bench("isCJKRegex ", () => isCJKRegex(input)); The subtle part is warmup phase. JavaScript JIT engines that you’re likely to use (v8 in Chrome, JavaScriptCore in Safari, SpiderMonkey in FireFox) have several optimization tiers and only do most aggressive optimization for frequently called functions. The warmup phase forces JIT to compile fn with most aggressive settings. This is only fair - I assume isCJK is called frequently. If it wasn’t, there’s no point sweating to optimize it. The new AI normal In the past I would have to manually implement each optimization idea. With AI I can delegate the coding and therefore test more ideas. The following was done with Claude 4.6. 100% vibe coding There are levels of delegating things to AI. On the extreme side of giving up control is 100% vibe coding. I had 2 ideas for performance optimization: early exit replace 15 linear checks with binary search But what if I didn’t? What if I just ask AI to come up with ideas? The following is my conversation with Claude 4.6 and what I’ve learned. I created is_cjk.js file and pasted the original function. look at is_cjk.js. come up with 2 ideas for how to speed up isCJK(). Implement those as separate functions and write a javascript micro-benchmark that compares those 2 new implementations against isCJK Two ideas: Pre-compiled regex — push the entire check into the native regex engine, avoiding JS-level for…of iteration and codePointAt() overhead. Typed array bitmap for BMP — for BMP characters (the vast majority of real text), a Uint8Array bitmap gives O(1) lookup with a single array access. Fall back to range checks only for supplementary plane characters. I have to admit: I recognize those as valid optimization ideas but they didn’t immediately occur to me. Regex works because regex implementations in JavaScript engines are insanely optimized. Bitmap is a known optimization for doing true/false lookup for numbers. Let’s say you have total range 0-255 and you want a function that checks if a number is in range 1-10, 20-30, 40-50, 60-70, 80-90. That’s 5 if statements, each doing 2 comparisons. You can instead use 1 bit for each of the 256 numbers (i.e. 256⁄8 = 32 bytes), pre-compute the array of bytes and set the bit for the ranges. Then lookup is just math to get byte offset and bit, load the byte, check the bit. Super fast. Claude not only wrote the benchmark function, 2 new implementations but also ran the benchmarks using node and showed me results in a readable way. The bitmap approach was significantly faster. Human assisted coding Bitmap was the fastest and yet I discarded it using my human judgement. The bitmap needed 65kB of memory and that’s just too much for this function. That’s where human judgement comes in to improve collaboration with AI. I didn’t specify that I also care about memory usage so AI wrote fast code that was using too much memory. You’re benchmarking what? Here’s another tricky part of benchmarking: knowing what you benchmark. There are 3 popular JavaScript engines. Claude ran benchmarks using node which uses Chrome’s v8. That’s good because that’s the most popular browser and therefore most popular JavaScript JIT engine. It’s also good to sanity-check with at least one other JavaScript engine. bun uses Safari’s JavaScriptCore so I asked Claude: run the tests using bun The results were similar, which is good. We don’t want code that is fast in one engine but slow in another. We don’t control which browser the user of our code is running. Continuing collaboration with AI I had some more ideas so here’s the rest of my conversation with Claude. don’t benchmark bitmap, it uses too much memory; write a variant of isCJK that does an early false exit if char is less than smallest Early exit is my first insight I wanted to test. As expected, early exit is good 2x speedup for latin strings, although regex beats it on cjk strings. That is the curse of engineering trade-offs: you have to decide if you want to optimize for cjk strings or non-cjk strings. now implement a variant that does inline bisect / binary search That was my second idea: do a binary search instead of sequential if comparisons. It was faster than original but slower than regex / early exit. Also, Claude didn’t do what I meant. It stored the ranges as array: // Flat array: [lo0, hi0, lo1, hi1, ...] sorted by lo const cjkRanges = new Uint32Array([ 0x3000, 0x303f, 0x3040, 0x309f, ... That’s what I meant: implement a variant isCJBisect that doesn’t use array but unrolls binary search logic into if statements The implementation is gnarly: function inCJKUnrolled(c) { if (c < 0x3000) return false; if (c < 0xff00) { if (c < 0x3400) { if (c < 0x3040) { return c <= 0x303f; } else if (c > 0x309f) { return c >= 0x30a0 && c <= 0x30ff; } return true; // ... more gnarly code But what do I care? Claude wrote it. Arguably it’s hard for me to review for correctness but it’s perfectly testable. We can write a test to compare if it returns the same result as original function for every possible input. As expected, unrolled version was faster than array-based but not always faster than regex or early exit. But Claude wrote something that caught my eye: The for…of + codePointAt iterator overhead remains the bottleneck shared by all non-regex variants on long strings. It’s well known that for ... of loop is slower than for (let i = 0; i < n; i++). It’s one of those things that you need to know. I knew this but I didn’t connect the dots in this particular case. write variant of isCJKEarly which replaces for of with a faster loop Result: isCJKEarlyFast crushes everything across the board on both runtimes The code for isCKJEarlyFast. Looks like we have a winner: Replacing for…of + codePointAt() with charCodeAt() + index loop eliminates the iterator overhead, which was the main bottleneck. The early c < 0x3000 bail then skips all range checks for ASCII/latin characters cheaply. Only regex still wins on Node’s long latin miss case thanks to V8’s native string scanning. There’s a subtlety that makes the rewrite of for .. of into for (let i = 0; i < n; i++) tricky. Logically JavaScript strings are composed from 16-bit numbers (characters). Some Unicode code-points are bigger than 16-bit and are stored as 2 16-bit numbers, called surrogate pairs. So writing a loop over unicode codepoints needs to take this into account. It’s tricky code but again, what do I care? Claude wrote it and it’s perfectly testable. Pressing on The good thing about claude writing most of the code and removing other tedium like running the benchmarks is that we can do more in the same amount of time. So I explored even more. I noticed: export function isCJKUnrolled(s) { for (const ch of s) { if (inCJKUnrolled(ch.codePointAt(0))) return true; } return false; } What happens when inCJKUnrolled is inlined? inline inCJKUnrolled into isCJKUnrolled Result: Done. Inlining didn’t help — it actually got slightly slower in most cases (the engine was already inlining the small function). No change in rankings; EarlyFast remains the winner. Turns out it’s not faster, which was surprising. Inlining is supposed to remove overhead of function calls, which matters in loops. Surprising isn’t bad. We run benchmarks to validate our hunches. For completness, I wanted to compare with original where for .. of was rewritten. also write original with for of replaced by standard for loop and bench all and give results as markdown table Claude was nice enough to show benchmark results in a nicely formatted table, without me even asking. It just knows things. But as you can see above, you can ask it for results in markdown table to e.g. include in a GitHub bug report. Here are final results. The number is operations/second in millions. Higher is better. Bun Scenario Original Regex Early Bisect Unrolled EarlyFast ForLoop Single CJK 11M 22M 12M 12M 16M 79M 57M Single latin 20M 24M 49M 21M 22M 40M 36M CJK string 12M 18M 17M 12M 14M 42M 42M Latin string 1.5M 4.7M 3.6M 3.0M 3.5M 17M 6.5M Mixed string 6.5M 5.9M 9.9M 8.6M 13M 62M 34M Node Scenario Original Regex Early Bisect Unrolled EarlyFast ForLoop Single CJK 85M 54M 51M 44M 56M 112M 110M Single latin 59M 72M 83M 55M 74M 115M 103M CJK string 79M 55M 60M 55M 64M 90M 129M Latin string 4.2M 57M 4.3M 2.8M 4.5M 12M 8.3M Mixed string 14M 15M 16M 10M 15M 41M 38M Conclusions AI is a big unlock. It took me under 30 minutes to test various hypotheses and find out a significant speed up. Without Claude it would take several hours and I would likely not do it at all. It’s just not important enough to spend a working day on it. To get best results we still need to apply human judgement and guide the AI. Programming expert knowledge An expert is simply someone who knows things. We know things because we learn them. If you were paying attention you might have learned the following things: importance of warmup phase when benchmarking JIT compilers for .. of is slower than for (let i = 0; i < n; i++) regex matching in JavaScript engines is fast subtlety of surrogate pairs in JavaScript strings using bitmaps to speed up range lookups Resources All the code is in https://gist.github.com/kjk/bdbea9d90c3bb0454fbe26353c521bfd I like to write fast code. If you want a fast bookmark manager / note taker, try MarkLexis. If you want a fast PDF / ebook / comic book reader for Windows, try SumatraPDF.
So you’ve installed Visual Studio and you want to run the compiler cl.exe from command-line. Microsoft makes it surprisingly hard. They give you a shortcut which opens a terminal window with cmd.exe setup for compilation. But I don’t want a separate window, I want to use the terminal app. You can run cmd.exe /k "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 (location for your setup can be different). But I don’t want to run inside cmd.exe. I want to use powershell. What exactly does vcvarsall.bat do? Not much: it just sets some env variables and updates PATH. We can reverse-engineer what it does: cmd.exe set >before.txt cmd.exe /k "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 set >after.txt Now compare before.txt and after.txt to see what changed. I asked AI to do it for me and here’s the beginning of what I found: CommandPromptType=Native DevEnvDir=C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\ ExtensionSdkDir=C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs EXTERNAL_INCLUDE=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\include;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\ATLMFC\include;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include;C:\Program Files (x86)\Windows Kits\10\include\10.0.26100.0\ucrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\um;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\shared;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\winrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\cppwinrt;C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um Framework40Version=v4.0 FrameworkDir=C:\Windows\Microsoft.NET\Framework64\ FrameworkDir64=C:\Windows\Microsoft.NET\Framework64\ ... more stuff I saved that to diff.txt file. Now that we have that we can ask our friendly AI to convert this to a powershell function: write powershell function that sets variables as seen in @diff.txt I get something like this: function set-msvc { <# .SYNOPSIS Sets Visual Studio Developer Command Prompt environment variables. .DESCRIPTION This function sets all the environment variables needed for Visual Studio 2022 native x64 development, equivalent to running vcvars64.bat. .EXAMPLE Set-VsDevEnv #> [CmdletBinding()] param() $env:CommandPromptType = "Native" $env:DevEnvDir = "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\" ... more stuff We need to change one thing: $env::PATH from absolute to just adding paths to current $env::PATH so that it’ll adapt to future changes in PATH. Now add that to $PROFILE and you can type set-msvc to setup powershell to have access to cl.exe, msbuild and other tools. Below is the whole function but note that this is only for amd64 toolset. For 32-bit or arm setup would be different. You can get by re-running the above logic for vcvarsall.bat with different arguments. function set-msvc { <# .SYNOPSIS Sets Visual Studio Developer Command Prompt environment variables. .DESCRIPTION This function sets all the environment variables needed for Visual Studio 2022 native x64 development, equivalent to running vcvars64.bat. .EXAMPLE Set-VsDevEnv #> [CmdletBinding()] param() $env:CommandPromptType = "Native" $env:DevEnvDir = "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\" $env:ExtensionSdkDir = "C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs" $env:EXTERNAL_INCLUDE = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\include;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\ATLMFC\include;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include;C:\Program Files (x86)\Windows Kits\10\include\10.0.26100.0\ucrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\um;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\shared;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\winrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\cppwinrt;C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" $env:Framework40Version = "v4.0" $env:FrameworkDir = "C:\Windows\Microsoft.NET\Framework64\" $env:FrameworkDir64 = "C:\Windows\Microsoft.NET\Framework64\" $env:FrameworkVersion = "v4.0.30319" $env:FrameworkVersion64 = "v4.0.30319" $env:FSHARPINSTALLDIR = "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools" $env:HTMLHelpDir = "C:\Program Files (x86)\HTML Help Workshop" $env:INCLUDE = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\include;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\ATLMFC\include;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include;C:\Program Files (x86)\Windows Kits\10\include\10.0.26100.0\ucrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\um;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\shared;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\winrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\cppwinrt;C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" $env:is_x64_arch = "true" $env:LIB = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\ATLMFC\lib\x64;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\lib\x64;C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\lib\um\x64;C:\Program Files (x86)\Windows Kits\10\lib\10.0.26100.0\ucrt\x64;C:\Program Files (x86)\Windows Kits\10\\lib\10.0.26100.0\\um\x64" $env:LIBPATH = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\ATLMFC\lib\x64;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\lib\x64;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\lib\x86\store\references;C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0;C:\Program Files (x86)\Windows Kits\10\References\10.0.26100.0;C:\Windows\Microsoft.NET\Framework64\v4.0.30319" $env:NETFXSDKDir = "C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\" $env:Path = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\bin\HostX64\x64;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\VC\VCPackages;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer;C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\bin\Roslyn;C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\;C:\Program Files (x86)\HTML Help Workshop;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools;C:\Program Files\Microsoft Visual Studio\2022\Community\Team Tools\DiagnosticsHub\Collector;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin;C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\\x64;C:\Program Files (x86)\Windows Kits\10\bin\\x64;C:\Program Files\Microsoft Visual Studio\2022\Community\\MSBuild\Current\Bin\amd64;C:\Windows\Microsoft.NET\Framework64\v4.0.30319;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\;" + $env::Path + ";C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\VC\Linux\bin\ConnectionManagerExe;C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg" $env:Platform = "x64" $env:UCRTVersion = "10.0.26100.0" $env:UniversalCRTSdkDir = "C:\Program Files (x86)\Windows Kits\10\" $env:VCIDEInstallDir = "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\VC\" $env:VCINSTALLDIR = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\" $env:VCPKG_ROOT = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg" $env:VCToolsInstallDir = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\" $env:VCToolsRedistDir = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.44.35112\" $env:VCToolsVersion = "14.44.35207" $env:VisualStudioVersion = "17.0" $env:VS170COMNTOOLS = "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\" $env:VSCMD_ARG_app_plat = "Desktop" $env:VSCMD_ARG_HOST_ARCH = "x64" $env:VSCMD_ARG_TGT_ARCH = "x64" $env:VSCMD_VER = "17.14.24" $env:VSINSTALLDIR = "C:\Program Files\Microsoft Visual Studio\2022\Community\" $env:WindowsLibPath = "C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0;C:\Program Files (x86)\Windows Kits\10\References\10.0.26100.0" $env:WindowsSdkBinPath = "C:\Program Files (x86)\Windows Kits\10\bin\" $env:WindowsSdkDir = "C:\Program Files (x86)\Windows Kits\10\" $env:WindowsSDKLibVersion = "10.0.26100.0\" $env:WindowsSdkVerBinPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\" $env:WindowsSDKVersion = "10.0.26100.0\" $env:WindowsSDK_ExecutablePath_x64 = "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\" $env:WindowsSDK_ExecutablePath_x86 = "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\" $env:__DOTNET_ADD_64BIT = "1" $env:__DOTNET_PREFERRED_BITNESS = "64" $env:__VSCMD_PREINIT_PATH = "C:\Program Files\PowerShell\7;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\Microsoft VS Code\bin;c:\Users\kjk\AppData\Local\Programs\cursor\resources\app\bin;C:\Program Files\gs\gs10.03.1\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\GitHub CLI\;C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;C:\Program Files\Rust stable MSVC 1.88\bin;C:\Program Files\PowerShell\7\;C:\Program Files\nodejs\;C:\Program Files\dotnet\;C:\Program Files\CMake\bin;C:\Program Files\Go\bin;C:\Program Files\RedHat\Podman\;C:\Program Files\Tailscale\;C:\Program Files\Git\cmd;C:\Program Files\Docker\Docker\resources\bin;C:\Users\kjk\AppData\Local\Microsoft\WindowsApps;C:\Users\kjk\AppData\Local\Microsoft\WinGet\Links;C:\Users\kjk\.bun\bin;C:\Users\kjk\.dotnet\tools;C:\Users\kjk\go\bin;C:\Users\kjk\AppData\Local\Programs\superfile\;C:\Users\kjk\AppData\Local\Microsoft\WindowsApps;C:\Users\kjk\AppData\Local\GitHubDesktop\bin;C:\Users\kjk\AppData\Local\Programs\cursor\resources\app\bin;C:\Users\kjk\AppData\Roaming\npm;C:\Users\kjk\.dotnet\tools;C:\Users\kjk\AppData\Local\Programs\Antigravity\bin;C:\Users\kjk\AppData\Local\Programs\Zed\bin;C:\Users\kjk\go\bin;C:\Users\kjk\OneDrive\bin;C:\Users\kjk\.bin\jai\bin;C:\Users\kjk\.bin\mupdf-1.27.0;C:\Users\kjk\.local\bin;C:\Users\kjk\AppData\Local\Programs\WinMerge;C:\Users\kjk\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\Scripts;C:\Users\kjk\OneDrive\bin\sublime_text" Write-Host "Visual Studio 2022 Developer Environment configured for x64." -ForegroundColor Green }
More in programming
I’ve been taking a break from Mastodon as my social home on the web for about six weeks. It has been more difficult than I’d usually admit. I have an amazing local community in my town that I love and am deeply involved in. But nobody here is a programmer, or even a computer nerd, so it doesn’t serve my need for a tech community. I work alone, for myself, in my house. It’s a fortunate lifestyle with many upsides, but it means I don’t get that form of community at work, either. I’ve had this feeling before… as a teenager in the 1990s. It’s profoundly lonely. But when I got internet access, that started to change. I found my people. First in the Total Annihilation community (RIP, Annihilated), then the HardOCP Forum, then the Something Awful Forums, then Tumblr, then Twitter and our community of Apple bloggers, and then Mastodon. Communities form, change, and dissolve over time. None of those are, today, anything like what they were when I most enjoyed them. We change, too. I didn’t move on from most of those solely because they changed — it was usually because we both changed, and it was no longer a good fit. My break from Mastodon has been a relief in some ways, as I’ve felt increasing divergence between what I wanted and what I was getting from it, but I didn’t know where else to go. Instagram, TikTok, and YouTube are all more video-broadcast platforms than communities. All I’ve found on Bluesky is even more anger and politics, the last things I need or want. Threads hasn’t yet stuck for me, and it’s owned by an asshole. Twitter was an amazing product and community that was previously owned by assholes, and now it’s X, which has a strong but different tech community, and is owned by different assholes. So I’ve just been… nowhere, for the first time since the 1990s. It has been, once again, quite lonely. Sure, it’s been relaxing, too, but in the way a vacation is relaxing. It’s great to take breaks, but after a long vacation, I’m ready to turn my brain back on. Without participating in a large, broad tech community, I’m exposed to fewer new ideas, both of my main jobs (app development and podcasting) are much harder, the quality and reach of my work have suffered, my skills aren’t improving as much, my mind is less stimulated, and I feel disconnected from my professions and passions. My world has gotten smaller. I could keep going like this to prove a stubborn victory or purity test, but I’d mostly just be hurting myself. Communities change, conditions change, and we change. We’re all better off if we’re willing to change our minds, and if our communities let us, no matter how strongly our younger selves declared a previous position. So I’m going to wade back in and see how it goes, trying to find a better balance to hopefully have better results, including on Mastodon. My people are spread across multiple services, and I’m going to explore them all, despite most of them being owned by assholes. I’d love to participate in communities without any assholes benefiting from it, but in the modern world, that simply isn’t possible. The only alternative is to isolate myself and not have a community at all (or a job, or a smartphone, or a computer, or internet access, or a car, or most of my food, or my country). That’s neither a price I’m willing to pay nor a condition I’d try to impose on anyone else. I’d love to have your support, forgiveness, and grace as I try to find my people again, and I strive to offer the same to all of you. Everyone deserves to be part of a community. And as I grow, I’m learning to declare fewer absolutes in a complex, imperfect, and constantly changing world.
My now-traditional “summary” of the conference along with my own commentary and thoughts.
Every year, browser vendors collaborate via the Interop project to synchronize their efforts on new HTML/CSS/JS features. This year, there’s a ton of super exciting features being considered! In this blog post, we’ll explore the CSS of the future, to get a glimpse of what we’ll soon be able to do.
Imprint’s average engineer is merging 4x more PRs than 2 years ago. Yay. Aggregated behind the average is something even more interesting: we have two teams that are merging 10x their PRs over that period. One engineer is merging 25x the PRs per week of our average engineer two years ago, and a number are merging 10x. That’s pretty neat, and the slope of the line suggests our average in a year is going to look much closer to today’s p90 than today’s p50. Even more importantly, we aren’t just shipping more, our quality has improved: incidents are down 42% half-over-half, and critical incidents are down 78% half-over-half. I think we’ve always known the quality/velocity tradeoff wasn’t a particularly useful construct, but that’s undeniable today. As we’ve walked this path, we–like everyone else progressing down it–have run into the problem of pull request review. The p50 pull request review took about 33 minutes to complete 2 years ago, but now takes us about 77 minutes. The slope here correlates tightly with the number of pull requests getting merged. Tightly enough that addressing it became one of my priorities for the week. We solved it! Just kidding, we’ve absolutely not solved it, but I think our current approach is an interesting one, which we call the small-change. The work discussed here was shaped and done by many folks at Imprint including Dan, Joy, Daniel, Lenny, Taylor, Ted, and Annie, among many others. Why review pull requests? There are four reasons we review pull requests: Knowledge sharing between author and reviewer – software engineering is generally learned via apprenticeship, and reviews are a primary mechanism of experienced engineers teaching others what good software looks like. We believe the current volume means that this knowledge sharing will have to move towards operating on clusters of pull requests, rather than individual pull requests Preventing errors – review has always been an expensive time to catch errors, and we believe that effective CICD along with harness verification techniques like poteto’s pstack are more effective mechanisms for preventing errors than review Preventing malicious behavior – my experience is that even careful code review will generally miss competent malicious behavior, although fortunately most malicious behavior is incompetent. We believe both sorts of behavior are best identified using a combination of fixed rules (modifying these specific objects/classes/files requires review), anti-abuse mechanisms that we’ve refined in our LLM-based member support experiences, auditing, and audit log inspection Compliance obligations in our documented controls – these can be changed, but today they require that an independent human reviews changes in a subset of our repositories In the modern development paradigm, we think there is a better solution for each of these problems. But, we also want to make these changes very cautiously. Small steps with small-change We believe we move fast by finding narrow, contained ways to validate new practices, so to iterate our way into an effective solution, we’ve developed the concept of a small-change. Here’s how it works: When a pull request is marked ready, CICD determines whether it is eligible to be a small-change. This is represented as a passing Github check. There is also a label for visibility, but it is the check itself, managed by the build and not human modifiable, that determines eligibility. (The label is used for initial filtering, but the check is still independently verified.) Something is a small-change if it meets all of these criteria: It is less than 1,000 lines of code There are no p1 or p2 findings from our ai-code-review automated review bot The author has “experience” in the service or application, defined as 10+ prior merged pull requests into that service or application’s directories. For changes working across multiple services or applications, the author must have experience in all of them to qualify Impacts fewer than N distinct services or applications. N is 1 on our frontend monorepo, and N is 3 in our backend monorepo, where typical changes will modify both a gRPC service and a common HTTP gateway Shared libraries and utilities files are not eligible for small-change Each repository has a .small_change file which allows per-path customization while we continue to tune the behaviors. For example, a service dealing with money movement can be excluded Our Slack automation tags every reference to a pull request with reacji labeling the state of that pull request, for example if it has failing tests, if it has been approved but tests are still running, or if it is ready to merge. We added a small-change reacji that indicates which of those pull requests meet the small-change criteria In our internal skills repository, we created two skills that have been distributed across harnesses: review-small-change which takes a list of repositories and reviews all open, non-draft, small-changes, including providing approval if they meet the review’s criteria (all checks pass including small-change check, and not touching on sensitive topics such as payments and security) review-pr-cluster which reviews batches of pull requests to identify common themes that came up, and suggest opportunities for discussion and adding verifications We set the social expectation that any error caused by a small-change is a failure in our system, and has to be solved at the system level: improvements to ai-code-review, to our verification mechanisms, to our CICD, to our harness, etc. This took about two days of infrequent attention to implement and rollout, although I imagine we will be tuning the knobs here for a long time. Maybe forever. fleet: centrally orchestrated harness I’ll briefly mention our other approach to reducing code review volume, which anchors on our centrally-orchestrated harness, fleet. (Which is similar to Stripe’s Minions although not following the one-shot approach in particular). We rely on fleet to implement and verify software in a highly controlled harness running in the cloud, with independent evaluation mechanisms to identify the correct number of independent reviewers for any given change. That number can be as low as one (e.g. fleet writes, one human reviews) for changes evaluated as safe. Results / next steps / reception I’ll be honest, it’s too early to give a useful result here. From early results, about 10% of changes qualify as a small-change. Those are now eligible for very fast approval, and generally get it unless they trip a flag, generally due to touching a security or money adjacent topic. While 10% is a small percentage, the core concept here is working, and ultimately will succeed or fail based on our ability to expand that percentage over the next few months in tandem with expanding our verification mechanisms to be increasingly comprehensive. This is ultimately going to fail or succeed depending on our ability to improve our verifications, which does require more difficult sorts of techniques to accomplish in low scale-complexity, high domain-complexity software than in the high scale-complexity, low domain-complexity products where many techniques like incremental rollouts are exceptionally effective. (Don’t get me wrong, they still work, they just provide less coverage when most users have different configuration and behavior.) Folks have not been uniformly excited about this, it’s a substantial change from how we’ve learned to be good engineers, and it’s uncomfortable to reevaluate whether a long-held best practice has become merely dogma. The most energizing part of the current cycle is that we get to reevaluate these practices we grew up on every week; the most painful part of the current cycle is that we’re likely to fall behind if we ever stop reevaluating them.