ÇalışmalarAçık kaynak geliştirici aracı
BuildMeter: Build Beklerken Geçen Zamanı Ölçmek
Flutter, .NET, React ve Next.js build'lerini terminalden, IDE'lerden ve editörden toplayıp macOS menü çubuğunda, widget'ta ve Windows tray uygulamasında günlük, haftalık ve aylık özetlere dönüştüren açık kaynak geliştirici aracı.
Problem
Uygulamayı cihazda açmak, dev sunucusunun ayağa kalkmasını beklemek, bir .NET çözümünü yeniden derlemek. Bunların her biri gün içinde onlarca kez tekrarlanan, birkaç saniye ya da birkaç dakika süren beklemeler. Tek tek dikkat çekmiyorlar ama toplandıklarında ciddi bir süre ediyorlar ve genellikle kimse bu süreyi ölçmüyor.
Ölçmeye çalıştığımda iki sorunla karşılaştım. İlki, komutun bitişinin işe yaramaz bir ölçüt olması:
flutter run, next dev veya dotnet run uygulama açıldıktan sonra da çalışmaya devam ediyor.
Benim beklediğim süre, uygulamanın kullanılabilir hale geldiği ana kadar geçen süre.
İkincisi, build’lerin tek bir yerden başlamaması. Aynı gün içinde bir Flutter projesini terminalden, bir .NET API’sini Rider’dan, bir Next.js uygulamasını VS Code’dan çalıştırıyorum. Bunlardan yalnızca birini ölçen bir araç resmin küçük bir parçasını gösteriyor.
Bulduğum araçlar ya tek bir IDE’ye ya da tek bir build sistemine bağlıydı; bazıları da bekleme süresi yerine CPU süresini raporluyordu. Benim istediğim, hangi teknolojiyi, aracı ya da işletim sistemini kullanırsam kullanayım “bugün build beklerken ne kadar zaman geçirdim?” sorusuna dürüst cevap veren bir araçtı.
Hedef
Araçtan beklentilerim şunlardı:
- Komutlar her zamanki gibi yazılmalı, geliştiricinin alışkanlığı değişmemeli.
- Çalıştırma komutlarında uygulamanın hazır olduğu an yakalanmalı.
- Terminal, MSBuild, Rider, Visual Studio ve editör kayıtları aynı veri modelinde toplanmalı.
- Aynı build’i birden fazla toplayıcı görse bile build bir kez sayılmalı.
- Eşzamanlı build’ler toplam bekleme süresinde iki kez sayılmamalı.
- macOS, Windows ve Linux’ta çalışmalı.
- Veriler yerelde kalmalı, hiçbir sunucuya gönderilmemeli.
Mimari: tek dosya, çok toplayıcı
Sistemin merkezinde bilerek basit tuttuğum bir sözleşme var: ~/.buildmeter/events.jsonl. Her
toplayıcı bu dosyaya satır satır start ve end olayları ekliyor; arayüzler yalnızca bu dosyayı
okuyor.
Terminal wrapper (Go) ─┐
MSBuild hook ──────────┤
Rider eklentisi ───────┼──▶ ~/.buildmeter/events.jsonl ──▶ macOS uygulaması + widget (Swift)
Visual Studio VSIX ────┤ ├─▶ Windows tray uygulaması (C#)
Editör eklentisi (JS) ─┘ └─▶ buildmeter report (CLI)
Toplayıcıların birbirini tanıması gerekmiyor. Yeni bir kaynak eklemek, aynı satır biçimini yazan bir bileşen eklemekten ibaret. Dosya ekleme kipinde açıldığı için eşzamanlı build’ler kilit olmadan kayıt düşebiliyor. JSON Lines bozulmaya karşı da dayanıklı: yarım kalmış ya da bozuk bir satır yalnızca o satırın atlanmasına yol açıyor.
Birden fazla makinede çalışırken events-<host>.jsonl dosyaları aynı klasörde birlikte
okunabiliyor ve aynı id’li kayıt bir kez sayılıyor.
Hazır anını yakalamak
Çalıştırma ve dev komutlarında ölçümü, çıktıda beliren “hazır” satırı bitiriyor. Hangi komutun
hangi profile karşılık geldiği ve hangi satırın hazır anlamına geldiği profiles.json adlı tek bir
tabloda tanımlı:
| Teknoloji | Komut | Kayıt ne zaman biter |
|---|---|---|
| Flutter | flutter run |
Flutter run key commands. |
| Next.js | next dev |
✓ Ready in |
| Vite | vite |
ready in … ms |
| Create React App | react-scripts start |
Compiled successfully |
| .NET | dotnet run |
Now listening on: / Application started. |
| .NET | dotnet watch |
dotnet watch 🚀 Started |
| Hepsi | … build |
Komutun bitişi |
Terminal wrapper’ı da editör eklentisi de bu tabloyu okuyor, böylece Go ve JavaScript tarafındaki
eşleme mantığı zamanla birbirinden kopmuyor. npm run dev gibi paket yöneticisi komutları
package.json üzerinden asıl araca (vite, next …) çözülüyor. pnpm -C, npm -w ve npx gibi
varyasyonlar da destekleniyor.
Araçların sürümü değiştikçe çıktı metinleri de değişebiliyor. Bu yüzden Vite, Next.js, Flutter ve ASP.NET Core’un gerçek çıktılarını sürüm adıyla test fixture’ı olarak saklıyorum; testler hazır sinyalinin bu çıktılarda yakalandığını doğruluyor.
Terminal wrapper’ı
İlk sürüm bir bash fonksiyonuydu. Teknolojiler ve platformlar arttıkça bunu tek bir Go binary’sine
taşıdım. zsh, bash ve PowerShell tarafında yalnızca ince shim’ler kaldı: flutter, dotnet, npm,
vite gibi komutlar buildmeter track üzerinden çağrılıyor. Profile uymayan komutlar
(npm install, dotnet restore …) hiçbir kayıt yazılmadan olduğu gibi çalışıyor.
Wrapper’ın kullanıcıya hissettirmemesi gerekiyor. Komutun renkli çıktısı, interaktif tuşları
(r ile hot reload gibi) ve terminal boyutu olduğu gibi kalmalı. Bunun için Unix sistemlerde komut
bir pseudo-terminal içinde çalışıyor; pencere boyutu değişiklikleri ve sinyaller alt sürece
iletiliyor. Çıktı ekrana yazılırken aynı akış bir satır tarayıcısından da geçiyor.
Çıkış kodu da kayda giriyor. Ctrl+C ile kesilen oturumlar cancelled, hatayla biten oturumlar
failed olarak işaretleniyor. BUILDMETER_DISABLE=1 ölçümü tamamen kapatıyor.
Gerçek kullanımda beklemediğim bir sorun yapay zekâ destekli kodlama araçlarından çıktı. Claude
Code gibi araçlar shell profilini bir snapshot’a kopyalarken yalnızca belirli fonksiyonları
taşıyor; _ ile başlayan fonksiyonları ve shell değişkenlerini atlıyor. Bu yüzden shim’lere alt
çizgisiz isimler verdim ve binary yolunu her çağrıda yeniden çözmelerini sağladım.
.NET: MSBuild hook’u
.NET build’leri yalnızca terminalden gelmiyor; Rider ve Visual Studio da MSBuild’i kendileri
çağırıyor. Her IDE için ayrı ölçüm yazmak yerine MSBuild’in kullanıcı düzeyindeki ImportAfter
klasörüne bir .targets dosyası koydum. MSBuild bu dosyayı her projeye otomatik ekliyor, yani
dotnet build, Rider ve Visual Studio aynı noktadan geçiyor.
Hook gürültü üreten bazı build’leri kayda almıyor: IDE’lerin arka planda çalıştırdığı design-time build’ler, multi-target projelerin dış build’i (iç build’ler ayrı ayrı yazılıyor) ve BuildMeter’ın Visual Studio eklentisi yüklüyken Visual Studio içindeki proje build’leri.
Kaynak bilgisi MSBuild özelliklerinden geliyor: BuildingInsideVisualStudio Visual Studio’yu,
BuildingByReSharper Rider’ı gösteriyor. Bu sırada .NET 8 ile macOS’ta ImportAfter klasörünün
konumunun değiştiğini fark ettim; kurulum betikleri artık iki konumu da kontrol ediyor.
Aynı build’i bir kez saymak
Toplayıcı sayısı artınca en çok uğraştıran şey tekrarlanan kayıtlar oldu. VS Code’un terminalinde
yazılan flutter run komutunu hem wrapper hem editör eklentisi görebiliyor. dotnet run ise hem
wrapper’ın kaydını hem de çözümdeki her proje için ayrı MSBuild kayıtlarını üretiyor. Bunları
ayıklamak için birkaç kural koydum.
Wrapper bir editör terminalinde çalışırken görünmeyen bir OSC kaçış dizisi basıyor. Editör eklentisi bu işareti çıktıda görürse aynı komut için kayıt yazmıyor.
Editör eklentisi kendi açtığı terminallere BUILDMETER_SOURCE ekliyor. Wrapper ve MSBuild hook’u
bu değişkeni görünce kaydın kaynağını “terminal” yerine vscode, cursor veya antigravity
olarak yazıyor.
Wrapper bir .NET komutu başlattığında BUILDMETER_GROUP adında bir kimlik üretiyor. MSBuild hook’u
bu kimliği kendi kayıtlarına ekliyor ve aynı gruptaki bütün kayıtlar tek oturum olarak gösteriliyor.
Grubu olmayan MSBuild kayıtları ise aynı kaynak, makine ve çözüm için aralarında iki saniyeden kısa
boşluk varsa tek oturumda birleşiyor.
Başarısız bir MSBuild build’i end satırı yazmıyor. Açık kalan oturum, aynı kaynaktan yeni bir
build başladığında ya da 15 dakika sonra failed olarak kapatılıyor.
Raporlarda eşzamanlı build’ler toplam bekleme süresine bir kez katılıyor: iki proje aynı anda bir dakika derleniyorsa geliştirici toplamda bir dakika beklemiş oluyor. Düz toplam ile birleştirilmiş süre arasındaki fark bir dakikayı geçerse rapor ikisini birlikte gösteriyor.
Üç dilde aynı mantık, tek bir test seti
İstatistik mantığı üç ayrı yerde yazılı: macOS uygulamasında Swift, terminal raporunda Go, Windows uygulamasında C#. Üç implementasyon zamanla birbirinden ayrışırsa bunu fark etmek zor olur, çünkü her biri kendi içinde doğru çalışmaya devam eder.
Bunu önlemek için spec/fixtures/ altında platformdan bağımsız vakalar tanımladım. Her vaka bir
olay dosyası ile beklenen özetten oluşuyor. Vakaları bir Python betiği üretiyor ve üç dilin testleri
aynı vakaları doğruluyor:
| Vaka | Doğrulanan davranış |
|---|---|
| Eski kayıtlar | tech alanı olmayan kayıtlar Flutter sayılır |
| Bozuk satırlar | Bozuk ve yarım satırlar atlanır |
| Çoklu dosya | Farklı makinelerin kayıtları birlikte okunur; aynı id bir kez sayılır |
| Grup | Aynı group id’li kayıtlar tek oturumdur |
| MSBuild boşluğu | Grupsuz MSBuild kayıtları kısa boşlukta birleşir |
| Açık MSBuild | end gelmeyen oturum yeni build’de ya da 15 dk sonra failed olur |
| Örtüşme | Eşzamanlı build’ler birleştirilmiş toplamda bir kez sayılır |
GitHub Actions bu testleri macOS, Windows ve Linux’ta çalıştırıyor. Bir kural değiştiğinde önce fixture’ı güncelliyorum; değişikliği ancak üç implementasyon da aynı sonucu verdiğinde bitmiş sayıyorum.
Arayüzler ve raporlar
macOS uygulaması menü çubuğunda çalışan build’in canlı süresini, bugünün toplamını, son yedi günün grafiğini ve proje ile kaynak kırılımlarını gösteriyor. Küçük ve orta boy masaüstü widget’ları da aynı veriyi okuyor. Windows’ta aynı pencere bildirim alanından açılan bir tray uygulaması; build sürerken tray ikonunda geçen süre görünüyor.
Seçilen aralık (bugün, dün, bu hafta, son 7 gün, bu ay, geçen ay) için metin rapor panoya kopyalanabiliyor, oturumlar da CSV olarak dışa aktarılabiliyor. Aynı rapor terminalden de alınabiliyor:
buildmeter report --range week
buildmeter report --range month --tech dotnet --csv > dotnet.csv
Dağıtım
Kurulumu zahmetli bir geliştirici aracını kimse denemiyor. Bu yüzden her bileşeni kendi ekosisteminin alışılmış kanalından dağıtıyorum:
- Homebrew’da terminal wrapper’ı formula, macOS uygulaması cask olarak duruyor; tap her release’te otomatik güncelleniyor.
- Windows’ta wrapper, tray uygulaması ve MSBuild hook’u tek bir Scoop manifest’iyle kuruluyor.
- Editör eklentisi VS Code Marketplace ve Open VSX üzerinden VS Code, Cursor, Antigravity ve VSCodium kullanıcılarına ulaşıyor.
- GitHub Releases’ta evrensel macOS DMG’si, Windows için x64 ve arm64 paketleri, Rider eklentisi ve
Visual Studio
.vsixdosyası var.
Sonuç
BuildMeter sayesinde gün içine dağılan bekleme sürelerini artık görebiliyorum. Build terminalden, IDE’den ya da editörden başlamış olsun, proje Flutter, .NET, React ya da Next.js olsun, hepsi aynı zaman çizelgesinde ve aynı kurallarla raporlanıyor.
Teknik tarafta en çok emek isteyen kısım, birbirinden bağımsız çok sayıda toplayıcıyı tek bir dosya sözleşmesi ve ortak test vakalarıyla tutarlı tutmaktı. Go, Swift, C#, Kotlin ve JavaScript ile yazılmış bileşenler birbirini tanımadan aynı veriyi üretip tüketebiliyor.
Öğrendiklerim
- Neyi ölçtüğünüz, nasıl ölçtüğünüzden daha önemli. Komutun bitişini ölçmek kolay; işe yarayan veri, uygulamanın hazır olduğu an.
- Satır satır eklenen bir JSONL dosyası, çok sayıda bileşeni bir arada tutmaya yetti ve servis ya da kilit gerektiren alternatiflerden daha dayanıklı çıktı.
- Aynı kural birden fazla dilde yazılıyorsa test vakaları ortak olmalı. Üç implementasyonun aynı davrandığından emin olmanın güvenilir yolu, aynı fixture’ları üçünde de çalıştırmak.
- Veriyi toplamak kolay kısımdı. Zor olan, aynı build’i farklı kaynaklardan görüp tek oturuma indirmekti.
- Bir geliştirici aracı komut yazma biçimini değiştiriyorsa, çıktıyı bozuyorsa ya da kolayca kapatılamıyorsa bir süre sonra kaldırılıyor.
- Araçların çıktısı da bir API gibi davranıyor. Hazır sinyallerini gerçek çıktılarla test etmek, araç sürümü değiştiğinde ölçümün sessizce bozulmasını engelliyor.