İçeriğe geç
Haydar Demir

Ç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ı.

RepositoryLive

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ı:

  1. Komutlar her zamanki gibi yazılmalı, geliştiricinin alışkanlığı değişmemeli.
  2. Çalıştırma komutlarında uygulamanın hazır olduğu an yakalanmalı.
  3. Terminal, MSBuild, Rider, Visual Studio ve editör kayıtları aynı veri modelinde toplanmalı.
  4. Aynı build’i birden fazla toplayıcı görse bile build bir kez sayılmalı.
  5. Eşzamanlı build’ler toplam bekleme süresinde iki kez sayılmamalı.
  6. macOS, Windows ve Linux’ta çalışmalı.
  7. 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 .vsix dosyası 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.

Aramak için yazın