What's going on here? It appears to be a bug in the drain implementation of std.Io.Writer.Allocating. drain is the one method a Writer has to implement, and, besides self, it takes two parameters:
It takes a list of values to write (to support vectored I/O) and a "splat" count which is the number of times the last value in data should be written. splat is particularly useful, I believe, for compression. Here's a relevant line from zstd/Decompress.zig:
Where writer.splatByteAll finds its way to calling drain. So we have some idea of drain's parameters, but why does it grow so much. Here's a simplified version of Allocating's drain function:
Can you spot the issue? I couldn't, but Claude could. For the common case where data.len == 1 and splat == 1, we're actually reserving 2x the memory: once for the data and once for splat_len which is the data again (what the code calls the pattern). If we called drain(&.{"hello", " "}, 100), the code would need to reserve space 5 bytes for "hello" and 100 bytes for the pattern (" ".len * 100). But the implementation reserves the splat space for every value, including the pattern itself.
ArrayList doesn't suffer from this: it has no splat. If your use-case is simple, if you're just appending bytes, you might want to stick with it.
Windows Performance Analyzer (WPA) is one of the most powerful tools available for understanding system performance on Windows. It helps engineers investigate Event Tracing for Windows (ETW) traces across CPU, memory, disk, networking, scheduling, input, and many other areas of the operating system.
That power also comes with complexity. A single trace can contain a huge amount of data, and finding the right signal often requires knowing which WPA tables to open, which columns matter, how to filter the right time range, and how to connect multiple pieces of evidence into a root cause.
We are working on WPA MCP as an Early Preview feature to make that workflow easier and more approachable.
What is WPA MCP?
WPA MCP brings GitHub Copilot CLI into the WPA trace-analysis workflow. Instead of starting every investigation by manually navigating graphs and tables, you can ask questions about the trace in natural language. WPA MCP helps GiHub Copilot translate that intent into trace-data exploration, summarize relevant findings, and guide follow-up analysis.
GitHub Copilot can inspect available trace data based on the prompt & identify relevant tables and signals and provide an analysis that helps you decide where to look next.
Why we are building it
Performance analysis is often bottlenecked by expert knowledge. Experienced WPA users can move quickly, but new or occasional users may not know where to begin. Even experts can spend significant time correlating activity across views before they reach a useful conclusion.
WPA MCP is intended to reduce that friction. It :
Start investigations with plain-language questions.
Find relevant WPA tables, graphs, and time ranges faster.
Identify expensive CPU activity, call stacks, input delays, regressions, and other symptoms.
Ask follow-up questions without restarting the investigation.
Move from trace exploration to actionable insight more quickly.
An additional advantage is that you can use this for repetitive investigative workflows if your trace contains the same context. Download the latest preview version of WPA with MCP and use AI for trace analysis.
Current Early Preview support
The current Early Preview release supports GitHub Copilot only. Users need an active GitHub Copilot subscription to use WPA MCP. Other AI assistants, model providers, and MCP clients are not supported in this preview release.
Because this is an Early Preview, the user experience, supported scenarios, setup requirements, and feature behavior may change before general availability.
How it works
The Early Preview workflow is designed to fit GitHub Copilot CLI into WPA without replacing the existing analysis experience:
Open a trace in WPA.
Select the GitHub Copilot button in the upper-right corner of the WPA window.
Use the Copilot pane to ask a trace-analysis question.
Review the generated analysis and supporting signals.
Continue with follow-up questions to narrow the investigation.
Initial WPA window with GitHub Copilot CLI highlighted
WPA window GitHub Copilot CLI window
Example analysis flow
Start with a detailed prompt that describes the issue, including the observed symptoms, affected time range, and relevant user or system activity. Providing this context helps the LLM focus its investigation and produce a more relevant trace analysis.
Sample prompt below
Analyze this trace and tell me why the machine was slow. Prioritize evidence that explains perceived slowness such as hangs, delayed input response, long ready times, CPU starvation, disk bottlenecks, memory pressure, paging, service contention, and problematic drivers. Use the trace to distinguish between system-wide bottlenecks and activity isolated to a single app or service and create a digestible report based on the insights you received from the trace.
After the initial response, then can continue with more focused questions based on the output.
Which processes contributed most to the delay?
What call stacks were most expensive during the affected time range?
What evidence supports that conclusion OR Which tables supports this conclusion
Was the system CPU-bound, blocked on I/O, or waiting on another resource?
This conversational workflow does not remove the need to inspect the trace. Instead, it gives users a faster way to form hypotheses, find relevant evidence, and decide where to focus next.
As part of responsible AI safeguards, the MCP may request permission to access text or JSON files it generates. This experience will continue to evolve as new features are added.
Settings
With the release of WPA-MCP, we also have a set of new settings to add more support for the WPA MCP. You can access these Settings from the File Menu or from directly from the startup launcher screen.
WPA MCP Settings
MCP server
Server status – Provide information about the WPA MPC status.
Start with WPA – Controls whether WPA MCP starts automatically when GitHub Copilot CLI launches inside WPA. This setting is on by default. If disabled, start the server manually by entering /MCP and selecting WPA-MCP from the list of available MCP servers.
MCP tools – Provides a read-only access to the list of tools that WPA MCP uses to query the trace data.
Copilot CLI Settings
Play sound for Notifications – Simple as the title, it plays a sound when GitHub Copilot CLI is not in focus when enabled in the GitHub Copilot CLI settings.
Disabled MCP Server – Improve GitHub Copilot CLI startup performance by disabling MCP servers that are not needed for WPA investigations. Since WPA loads Copilot CLI with your configured MCP servers, disabling unused servers reduces startup time and reduce the noise in the context.
Screen Reader Optimization – This setting is to provide parity with screen reader optimization settings in GitHub Copilot CLI
Important notes about LLM-generated analysis
WPA MCP uses large language model capabilities through GitHub Copilot. LLM-generated responses can vary between runs and may be incomplete or incorrect. Users should treat Copilot output as an assistant-generated starting point, not as a final diagnosis.
Before taking action based on a result, users should validate the findings against the underlying WPA trace data. Review referenced tables, graphs, time ranges, processes, threads, and call stacks, and ask follow-up questions when the evidence is unclear.
What we want to learn during Early Preview
The goal of the Early Preview is to learn where AI assistance provides the most value in real WPA investigations. We are especially interested in feedback on prompt quality, trace-analysis accuracy, useful follow-up workflows, missing scenarios, and places where Copilot should expose more supporting evidence.
This preview is an early step toward making advanced Windows performance diagnostics more accessible, more scalable, and faster for a broader set of WPA users. Please provide feedback using the button besides the GitHub Copilot button or via Feedback Hub (WIN + F) under Settings > Start Menu.
Closing
WPA MCP is designed to help users spend less time searching through trace data and more time understanding what the trace is telling them. We are excited to share this Early Preview and continue improving the experience with feedback from Windows performance
I’m on vacation tomorrow, so probably no reading list. Which isn’t good, since I still have a queue of dozens of items to filter though. Maybe you’ll get a bonus weekend edition.
[article] What the Hell Is a Loop, Anyway? It’s not too late to ask. This author identifies four different “loops” that we’re talking about now. Maybe five?
I was a consultant for 23 years before I joined OpenSesame as their VP of Engineering. Now, three years later, I’m returning to consulting.
I enjoyed my time as VP. It gave me the opportunity to see my work play out over the long term, and it gave me fresh insights into the demands faced by executives.
So why return to consulting? Put simply: impact. I have the ability to see below surface symptoms to the interconnected web of causes and effects that lead to organizational outcomes, and the ability to change that system to make a difference. As one participant said about my work, “It was like a race had started... it was amazing.”
Making these systemic changes was just one part of my VP role. I also had career development, hiring interviews, business reviews, coordination meetings, strategy off-sites, and more. All important! And necessary for long-term success. Still, my calendar was packed. I’d like to say that I averaged 25% of my time on my key initiatives, but it was probably less.
As a consultant, when I’m working with a client, all my time is focused on their top priority. It’s faster. Much faster.
The power dynamic isn’t to be underestimated, either. As a VP, it’s always going to be an uphill battle to get folks to see past the title. They’ll bias towards protecting my time and hiding bad news. As a consultant, people want me to hear their perspective. They know it’s a safe way to make a difference.
I’m proud of my tenure at OpenSesame. We accomplished so much. We broke down silos, raised the bar for technical excellence, and focused engineering behind the company’s top priorities. I introduced a whole new approach to product engineering accountability. Oh, and the whole AI thing, too. I’ve got a whole list of stuff I’m eager to write about.
And I’m happy to return to consulting. I like the impact I can make. If that’s something that your company needs—if you’re missing strategic opportunities, and the fixes you’ve made haven’t helped—then let’s talk.
I’m available for speaking and podcast appearances, too. And I’m finally catching up on my email backlog! Maybe that’s the best thing of all.
Thanks, OpenSesame, for allowing me to spend three years with you. I’m looking forward to what’s next.
If you’ve been a software developer for more than five minutes, then you probably have some old code running somewhere. It probably works fine, so who cares if it’s a little dusty and based on an n-5 language version? Remember that upgrades are an important means for grabbing security patches and performance improvements. But those syntax and architectural changes are important too.
I wondered which popular languages provided deterministic tools for upgrading code to the latest version. Let’s see what Go, Java, C#, JavaScript, Python, and Rust have to offer.
Go offers the built-in go fix tool
Bias alert: I lead the product and engineering of Go at Google. But I’ve also used Go for years before that org shift happened a few months back.
Go includes syntax modernization directly inside the standard toolchain. It uses modular static analyzers to inspect packages and automatically rewrite legacy code patterns. Go projects are simple—no random XML or JSON settings, no “project” files—so this can be a focused, efficient tool.
Let’s see an example. Maybe I’ve got a CLI tool written in idiomatic Go 1.18 code. This CLI tool renames image files. The code does safe concurrency and uses a helper for slices.
package main
import (
"flag"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"strings"
"sync"
)
// Supported image extensions (Go 1.18 slice)
var imageExts = []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
// RenameRegistry ensures destination filenames are unique, resolving collisions thread-safely.
type RenameRegistry struct {
mu sync.Mutex
names map[string]int
}
func NewRenameRegistry() *RenameRegistry {
return &RenameRegistry{
names: make(map[string]int),
}
}
// GetUniqueName returns a collision-free filename in the destination directory.
func (r *RenameRegistry) GetUniqueName(base, ext, destDir string) string {
r.mu.Lock()
defer r.mu.Unlock()
count := r.names[base]
r.names[base] = count + 1
var targetName string
if count == 0 {
targetName = base + ext
} else {
targetName = fmt.Sprintf("%s_%d%s", base, count, ext)
}
// Double check disk existence to avoid overwriting existing files
for {
targetPath := filepath.Join(destDir, targetName)
if _, err := os.Stat(targetPath); os.IsNotExist(err) {
break
}
// If it exists, increment the counter and try again
count++
r.names[base] = count + 1
targetName = fmt.Sprintf("%s_%d%s", base, count, ext)
}
return targetName
}
// isImageFile checks if a file has a supported image extension (pre-slices inline check)
func isImageFile(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
for _, item := range imageExts {
if item == ext {
return true
}
}
return false
}
func main() {
// Parse CLI flags
srcDir := flag.String("src", ".", "Source directory containing photos")
destDir := flag.String("dest", "", "Destination directory for renamed photos (defaults to source)")
dryRun := flag.Bool("dry", false, "Dry run mode (lists proposed changes without executing)")
verbose := flag.Bool("verbose", false, "Enable verbose logging output")
flag.Parse()
// Default destination to source directory if not specified
if *destDir == "" {
*destDir = *srcDir
}
// Clean paths
*srcDir = filepath.Clean(*srcDir)
*destDir = filepath.Clean(*destDir)
log.Printf("Starting photorename CLI...")
log.Printf("Source directory: %s", *srcDir)
log.Printf("Destination directory: %s", *destDir)
if *dryRun {
log.Printf("DRY RUN MODE ENABLED - No files will be moved or renamed.")
}
// Scan source directory recursively
var photos []string
err := filepath.WalkDir(*srcDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && isImageFile(path) {
photos = append(photos, path)
}
return nil
})
if err != nil {
log.Fatalf("Error scanning source directory: %v", err)
}
totalPhotos := len(photos)
log.Printf("Found %d photo(s) to process.", totalPhotos)
if totalPhotos == 0 {
return
}
// Ensure destination directory exists (unless dry run)
if !*dryRun {
if err := os.MkdirAll(*destDir, 0755); err != nil {
log.Fatalf("Failed to create destination directory: %v", err)
}
}
registry := NewRenameRegistry()
var wg sync.WaitGroup
// Allocate worker pool limit using custom new helper (pre-Go 1.26 newexpr target)
limit := newInt(4)
// Limit concurrency using a semaphore (buffered channel)
sem := make(chan struct{}, *limit)
var successCount int
var successMu sync.Mutex
for _, photoPath := range photos {
photoPath := photoPath
wg.Add(1)
sem <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-sem }()
logDebug(*verbose, "[Processing] %s", photoPath)
// Get file info for modification time
info, err := os.Stat(photoPath)
if err != nil {
log.Printf("Error stating file %s: %v", photoPath, err)
return
}
// Format modification time as YYYYMMDD_HHMMSS
modTime := info.ModTime()
baseName := fmt.Sprintf("IMG_%s", modTime.Format("20060102_150405"))
ext := strings.ToLower(filepath.Ext(photoPath))
// Get unique target name to avoid collisions
targetName := registry.GetUniqueName(baseName, ext, *destDir)
targetPath := filepath.Join(*destDir, targetName)
if *dryRun {
log.Printf("[Dry-Run] Would rename: %s -> %s", photoPath, targetPath)
successMu.Lock()
successCount++
successMu.Unlock()
return
}
// Perform the rename/move operation
err = os.Rename(photoPath, targetPath)
if err != nil {
log.Printf("Rename failed for %s -> %s, attempting copy: %v", photoPath, targetPath, err)
} else {
logDebug(*verbose, "[Success] Renamed: %s -> %s", photoPath, targetPath)
successMu.Lock()
successCount++
successMu.Unlock()
}
}()
}
wg.Wait()
if *dryRun {
log.Printf("Dry run complete. Checked %d/%d photos.", successCount, totalPhotos)
} else {
log.Printf("Renaming complete. Successfully processed %d/%d photos.", successCount, totalPhotos)
}
}
// logDebug helper that prints debug logs using interface{} (pre-Go 1.18 any)
func logDebug(verbose bool, format string, args ...interface{}) {
if verbose {
log.Printf(format, args...)
}
}
// newInt is a custom helper to return a pointer to an int value (pre-Go 1.26 newexpr helper target)
func newInt(x int) *int {
return &x
}
On disk, my app contained a folder of randomly named pictures in various folders, as well as a misplaced text file that should be ignored:
After building and running the tool, it takes milliseconds to complete. And I end up with renamed files in a fresh folder.
Let’s modernize! I updated my go.mod to now reference Go 1.26.
go mod edit -go=1.26
Then ran the following command to do a dry run and see what it would change.
go fix -diff ./...
I get back a +/- view in the console that shows me which modernizers kicked in.
It won’t find EVERY opportunity to upgrade the code, as it looks for specific patterns. But in this case, it refactored legacy handling of slices, deleted some now-unnecessary code for loop variable shadowing, upgraded an interface parameter to any, and improved a pointer. All in under a second.
Go has a backwards compatibility promise, so it doesn’t need an aggressive modernizer. Instead, a first-party tool like go fix —which is an Abstract Syntax Tree (AST) that focuses on grammar and structural shape of source code—is meant for continuous codebase modernizations.
And I’ll note that Dart, the language that powers the popular Flutter framework, also has a pretty great dart fix command that does similar things. Google is pretty good at these things.
Java has built-in analysis tools, but relies on third party modernization tools
Modernizing Java code is a lot more than updating your build file! Bumping the version in your maven/gradle file simply tells the compiler what byte code to emit, but your source code doesn’t change.
The JDK does ship with diagnostic tools to help you identify upgrade issues, but these are read-only. No proactive rewrites. Useful JDK tools include:
jdeps: This scans your class files and JARs to report any static dependencies. It helps you get ready for migrations by finding deprecated features, broken third-party tools, and any references to restricted internal JDK APIs.
jdeprscan: Another static analysis tool that scans class files, directories, and JAR files for any use of deprecated Java APIs.
Many Java rewrites start in the IDE, like IntelliJ. There are some built-in analyzers and code fixing tools. You’re constrained to the GUI here, so people doing at-scale rewrites often use OpenRewrite. This build plug-in can upgrade syntax and do framework refactoring.
Given its established enterprise presence, Java has many documented modernization practices and a strong ecosystem of vendor-created modernization tools. But don’t look deeply into the built-in toolchain for a lot of support.
C# (.NET) provides a built in upgrade assistant
For some reason, .NET has had an (unnecessarily?) exciting journey. That means you need some industrial-strength modernization tooling to get you from one major iteration to the next.
Microsoft doesn’t bake any modernization tools directly into the language toolchain. They introduced the .NET Upgrade Assistant years ago, powered by their Roslyn compiler platform. You could run this sophisticated tool as a Visual Studio plug-in or via a standalone CLI. It scanned your code and then helped you upgrade to the latest version. It covered a lot of ground, because .NET projects are relatively heavy and the framework has seen some major architectural changes over the years.
However, this tool is now deprecated in favor of the GitHub Copilot modernization chat agent. That’s a very different solution. Now you have commercial implications that impact who can access it. It’s online-only, and internet-dependent. You’re not running this easily in a headless fashion. And now it’s non-deterministic, and not following preset rules. That’s also a “plus” as you’ll probably see deeper modernizations, self-correcting loops, and more flexibility. But some big token costs!
Like Java, C#/.NET has a massive community and plenty of well-documented practices. The ever-changing tools landscape means those practices aren’t timeless, but it’s not hard to find experts to help you modernize .NET apps. Just don’t look at the framework for a major assist.
JavaScript doesn’t offer built-in tools for modernization
You won’t find a built-in modernizer for JavaScript/TypeScript. It’s a fluid, fast-moving, and fragmented ecosystem with different pace layers. You’ve got the language-level updates (ECMAScript) and then the framework-level (React, Next.js, Angular, Vue). Instead of using heavyweight Node.js-native AST-style parsers, developers are embracing faster structural engines and framework CLI orchestrators.
These developers relied heavily on codemods, like jscodeshift. But that can be heavy and slow on giant repos. There’s a new class of Rust-based codemod engines like ast-grep, jssg, and GritQL.
JavaScript is probably too decentralized to offer a single, language-included code modernization tool. But the ecosystem has stepped up with useful options.
Python relies on third party modernizers
Plenty of devs are still scarred by the Python 2 to Python 3 migration. Python 3 came out in 2008, but Python 2 wasn’t retired until 2020. This wasn’t a simple version bump, but what felt like a whole different programming language. Python 3 did ship with an official AST-based modernization tool called 2to3. But it had issues, and didn’t make migrations easy.
Today’s Python doesn’t include any built-in source code rewriting tools. For any minor release upgrade, modernizing this code is entirely an ecosystem (third party) story. The built-in tooling basically covers diagnostics and signaling of deprecation. You get runtime warnings for deprecated API use, and compiler warnings for risky or outdated control flows. That’s about it.
Here’s where the ecosystem stepped up. You have syntax fixers like ruff that work at the base layer of the language. It includes many built-in rules and is used by many OSS projects. Another option is LibCST from Meta that preserves all the whitespace, inline comments, and code formatting which makes it good at complex refactorings. Then, like with JavaScript, there are framework-specific upgraders like django-upgrade for Django.
Rust offers a built-in cargo fix tool
Rust, like Go years earlier, added a native code modernizer in their toolchain. However, it’s quite different in how it works, despite doing conceptually-similar things.
cargo fix uses rustc compiler diagnostics to detect deprecated features or edition-incompatible idioms to drive changes to source files. A foundational part of Rust modernization relies on Editions. Rust uses an “Edition” approach (2015, 2018, 2021, 2024) to introduce potential opt-in breaking syntax changes without breaking the ecosystem. cargo fix was introduced to automate code modifications needed to transition crates (packages) from one Edition to the next.
I can’t run cargo fix to jump from a 2018 Edition to 2024. I’d execute a step-by-step path to first go to 2021, then to 2024. The tool’s goal is to modify code so that it’s valid in both the current Edition and target one simultaneously.
Of note, there are more modernization options than just cargo fix. Clippy is the official code linting tool for Rust. It analyzes source code to catch any design mistakes and improve your code. cargo upgrade also upgrades crate dependencies, while cargo outdated shows you a detailed tree of outdated dependencies. Our friend ast-grep also applies to Rust and can help you with codebase-wide refactoring.
Wrap Up
Does any of this matter now that we have LLMs? Can I just point Claude Code, Cursor, or Google Antigravity at a legacy code base and ask it to shine it up? Sort of.
Yes, you can use LLMs for some of this code modernization effort. But solely relying on an LLM? You’ll see a significant token spend, it’ll take magnitudes longer to execute a modernization, and you risk context truncation or degradation across massive files. It’s an “and” where you ideally couple LLMs with deterministic tools like some of the ones we listed here.
What did I get wrong? Do you have other favorite tools that help you modernize code written in your favorite language?