WASM ile Büyük Veri Setlerini Tarayıcıda İşleme

Büyük veri setleriyle uğraşmak zorunda kaldığımda, yıllarca şu döngüyü yaşadım: kullanıcı bir CSV yüklüyor, backend’e gönderiyoruz, işliyoruz, sonuç dönüyor. Kulağa mantıklı geliyor, değil mi? Ta ki dosya 500MB’ı geçene kadar. O noktada sunucu yükü tırmanıyor, timeout hataları yağıyor ve kullanıcı “neden bu kadar yavaş?” diye destek talebi açıyor. WebAssembly bu döngüyü kıran şey oldu benim için.

WebAssembly Nedir, Neden Büyük Veri İçin Önemli?

WASM, tarayıcı içinde native’e yakın hızda çalışan bir binary instruction formatıdır. JavaScript’in yerini almıyor, onun yanında çalışıyor. Ama kritik fark şu: CPU-yoğun işlemler için JavaScript’ten 10x-20x daha hızlı olabilir. Bir CSV dosyasını parse etmek, istatistik hesaplamak, veri filtrelemek, hatta makine öğrenmesi modeli çalıştırmak gibi işlemler için bu fark hayat kurtarıcı.

Pratik açıdan baktığımızda şunları kazanıyorsunuz:

  • Sunucu yükü sıfır: İşlem tamamen istemci tarafında gerçekleşiyor
  • Bant genişliği tasarrufu: Ham veriyi sunucuya yollamak yerine sadece sonucu alıyorsunuz
  • Gizlilik: Hassas veri hiç sunucuya gitmiyor
  • Ölçeklenebilirlik: 1000 kullanıcı aynı anda büyük dosya işlese bile sunucunuz etkilenmiyor

Rust ile WASM Modülü Yazma

Ben WASM geliştirmesinde Rust tercih ediyorum çünkü hem performansı hem de bellek güvenliği çok iyi. Ama C/C++ veya AssemblyScript de seçenek.

Önce araçları kuralım:

# Rust kurulu değilse
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# wasm-pack kur
cargo install wasm-pack

# Yeni proje oluştur
cargo new --lib csv-processor
cd csv-processor

Cargo.toml dosyasını düzenleyelim:

cat > Cargo.toml << 'EOF'
[package]
name = "csv-processor"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
csv = "1.3"
js-sys = "0.3"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
EOF

Şimdi gerçek işin yapıldığı Rust kodu:

cat > src/lib.rs << 'EOF'
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct DataSummary {
    pub row_count: usize,
    pub column_names: Vec<String>,
    pub numeric_stats: Vec<ColumnStats>,
}

#[derive(Serialize, Deserialize)]
pub struct ColumnStats {
    pub name: String,
    pub min: f64,
    pub max: f64,
    pub mean: f64,
    pub sum: f64,
    pub null_count: usize,
}

#[wasm_bindgen]
pub fn process_csv(csv_data: &str) -> JsValue {
    let mut reader = csv::Reader::from_reader(csv_data.as_bytes());
    let headers = reader.headers().unwrap().clone();
    
    let column_names: Vec<String> = headers
        .iter()
        .map(|h| h.to_string())
        .collect();
    
    let col_count = column_names.len();
    let mut sums = vec![0.0f64; col_count];
    let mut mins = vec![f64::MAX; col_count];
    let mut maxes = vec![f64::MIN; col_count];
    let mut counts = vec![0usize; col_count];
    let mut null_counts = vec![0usize; col_count];
    let mut row_count = 0;

    for result in reader.records() {
        let record = result.unwrap();
        row_count += 1;
        
        for (i, field) in record.iter().enumerate() {
            if i >= col_count { break; }
            match field.trim().parse::<f64>() {
                Ok(val) => {
                    sums[i] += val;
                    counts[i] += 1;
                    if val < mins[i] { mins[i] = val; }
                    if val > maxes[i] { maxes[i] = val; }
                },
                Err(_) => {
                    if field.trim().is_empty() {
                        null_counts[i] += 1;
                    }
                }
            }
        }
    }

    let numeric_stats: Vec<ColumnStats> = (0..col_count)
        .filter(|&i| counts[i] > 0)
        .map(|i| ColumnStats {
            name: column_names[i].clone(),
            min: mins[i],
            max: maxes[i],
            mean: sums[i] / counts[i] as f64,
            sum: sums[i],
            null_count: null_counts[i],
        })
        .collect();

    let summary = DataSummary {
        row_count,
        column_names,
        numeric_stats,
    };

    serde_wasm_bindgen::to_value(&summary).unwrap()
}
EOF

Derleme yapalım:

wasm-pack build --target web --out-dir pkg --release

# Çıktıyı kontrol et
ls -lh pkg/
# csv_processor_bg.wasm boyutunu görmek istiyoruz
# Release modda genellikle 200-400KB arası çıkar

JavaScript Tarafında WASM Entegrasyonu

WASM modülünü tarayıcıda kullanmak için birkaç yaklaşım var. Ben production ortamında Web Workers kullanmayı şiddetle tavsiye ediyorum çünkü UI thread’ini bloke etmemek kritik.

cat > worker.js << 'EOF'
import init, { process_csv } from './pkg/csv_processor.js';

let wasmInitialized = false;

async function initWasm() {
    if (!wasmInitialized) {
        await init();
        wasmInitialized = true;
    }
}

self.onmessage = async function(event) {
    const { type, data, id } = event.data;
    
    if (type === 'process_csv') {
        try {
            await initWasm();
            
            const startTime = performance.now();
            const result = process_csv(data);
            const endTime = performance.now();
            
            self.postMessage({
                id,
                success: true,
                result,
                processingTime: endTime - startTime
            });
        } catch (error) {
            self.postMessage({
                id,
                success: false,
                error: error.message
            });
        }
    }
};
EOF

Ana uygulama tarafında Worker ile iletişim:

cat > app.js << 'EOF'
class CSVProcessor {
    constructor() {
        this.worker = new Worker('./worker.js', { type: 'module' });
        this.pendingRequests = new Map();
        this.requestCounter = 0;
        
        this.worker.onmessage = (event) => {
            const { id, success, result, error, processingTime } = event.data;
            const pending = this.pendingRequests.get(id);
            
            if (pending) {
                this.pendingRequests.delete(id);
                if (success) {
                    pending.resolve({ result, processingTime });
                } else {
                    pending.reject(new Error(error));
                }
            }
        };
    }
    
    async processFile(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            
            reader.onload = async (e) => {
                const csvData = e.target.result;
                const id = ++this.requestCounter;
                
                this.pendingRequests.set(id, { resolve, reject });
                
                this.worker.postMessage({
                    type: 'process_csv',
                    data: csvData,
                    id
                });
            };
            
            reader.readAsText(file);
        });
    }
}

// Kullanım
const processor = new CSVProcessor();

document.getElementById('fileInput').addEventListener('change', async (event) => {
    const file = event.target.files[0];
    if (!file) return;
    
    const statusEl = document.getElementById('status');
    statusEl.textContent = 'İşleniyor...';
    
    try {
        const { result, processingTime } = await processor.processFile(file);
        
        statusEl.textContent = 
            `${result.row_count} satır işlendi. Süre: ${processingTime.toFixed(2)}ms`;
        
        displayResults(result);
    } catch (error) {
        statusEl.textContent = `Hata: ${error.message}`;
    }
});
EOF

Streaming ile Büyük Dosyaları Parça Parça İşleme

500MB+ dosyalar için tüm içeriği belleğe almak sakıncalı olabilir. Rust tarafında streaming desteği ekleyelim:

cat >> src/lib.rs << 'EOF'

#[wasm_bindgen]
pub struct StreamingProcessor {
    row_count: usize,
    column_sums: Vec<f64>,
    column_counts: Vec<usize>,
    headers_set: bool,
    column_names: Vec<String>,
    partial_line: String,
}

#[wasm_bindgen]
impl StreamingProcessor {
    #[wasm_bindgen(constructor)]
    pub fn new() -> StreamingProcessor {
        StreamingProcessor {
            row_count: 0,
            column_sums: Vec::new(),
            column_counts: Vec::new(),
            headers_set: false,
            column_names: Vec::new(),
            partial_line: String::new(),
        }
    }

    pub fn process_chunk(&mut self, chunk: &str) -> bool {
        let data = format!("{}{}", self.partial_line, chunk);
        let mut lines: Vec<&str> = data.split('n').collect();
        
        // Son satır tamamlanmamış olabilir
        self.partial_line = if !data.ends_with('n') {
            lines.pop().unwrap_or("").to_string()
        } else {
            String::new()
        };

        for line in lines {
            if line.trim().is_empty() { continue; }
            
            if !self.headers_set {
                self.column_names = line.split(',')
                    .map(|h| h.trim().trim_matches('"').to_string())
                    .collect();
                self.column_sums = vec![0.0; self.column_names.len()];
                self.column_counts = vec![0; self.column_names.len()];
                self.headers_set = true;
                continue;
            }

            let fields: Vec<&str> = line.split(',').collect();
            self.row_count += 1;
            
            for (i, field) in fields.iter().enumerate() {
                if i >= self.column_names.len() { break; }
                if let Ok(val) = field.trim().trim_matches('"').parse::<f64>() {
                    self.column_sums[i] += val;
                    self.column_counts[i] += 1;
                }
            }
        }
        true
    }

    pub fn get_progress(&self) -> usize {
        self.row_count
    }

    pub fn finalize(&self) -> JsValue {
        let stats: Vec<serde_json::Value> = self.column_names.iter().enumerate()
            .filter(|(i, _)| self.column_counts[*i] > 0)
            .map(|(i, name)| {
                serde_json::json!({
                    "name": name,
                    "sum": self.column_sums[i],
                    "count": self.column_counts[i],
                    "mean": self.column_sums[i] / self.column_counts[i] as f64
                })
            })
            .collect();
        
        let result = serde_json::json!({
            "row_count": self.row_count,
            "columns": stats
        });
        
        serde_wasm_bindgen::to_value(&result).unwrap()
    }
}
EOF

JavaScript tarafında streaming okuma:

cat > streaming.js << 'EOF'
import init, { StreamingProcessor } from './pkg/csv_processor.js';

async function processLargeFile(file) {
    await init();
    
    const processor = new StreamingProcessor();
    const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
    let offset = 0;
    
    const progressBar = document.getElementById('progress');
    const totalSize = file.size;
    
    while (offset < totalSize) {
        const chunk = file.slice(offset, offset + CHUNK_SIZE);
        const text = await chunk.text();
        
        processor.process_chunk(text);
        offset += CHUNK_SIZE;
        
        const progress = Math.min((offset / totalSize) * 100, 100);
        progressBar.value = progress;
        
        const rowCount = processor.get_progress();
        document.getElementById('rowCount').textContent = 
            `İşlenen satır: ${rowCount.toLocaleString('tr-TR')}`;
        
        // UI'ın nefes almasına izin ver
        await new Promise(resolve => setTimeout(resolve, 0));
    }
    
    const result = processor.finalize();
    return result;
}
EOF

Gerçek Dünya Senaryosu: Finansal Veri Analizi

Bir fintech projesinde bunu gerçekten kullandım. Müşteri, kendi banka ekstrelerini yükleyip harcama analizi görmek istiyordu. Veri hassas olduğundan sunucuya gönderilmesi söz konusu değildi. 200.000 satırlık CSV’yi tarayıcıda 800ms’de işliyoruz, sıfır ağ trafiği ile.

Deployment için nginx konfigürasyonu da önemli. WASM dosyaları için doğru MIME type ve CORS header’ları şart:

# /etc/nginx/sites-available/wasm-app
cat > /etc/nginx/sites-available/wasm-app << 'EOF'
server {
    listen 80;
    server_name app.example.com;
    root /var/www/wasm-app;
    
    # WASM için doğru MIME type
    types {
        application/wasm wasm;
    }
    
    # SharedArrayBuffer için gerekli headerlar
    add_header Cross-Origin-Opener-Policy "same-origin";
    add_header Cross-Origin-Embedder-Policy "require-corp";
    
    # WASM dosyaları için cache
    location ~* .wasm$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Cross-Origin-Opener-Policy "same-origin";
        add_header Cross-Origin-Embedder-Policy "require-corp";
        gzip off; # WASM zaten binary, gzip'in faydası yok
    }
    
    location / {
        try_files $uri $uri/ /index.html;
    }
}
EOF

nginx -t && systemctl reload nginx

Performans Karşılaştırması ve İzleme

Performansı ölçmek ve izlemek için basit bir benchmark scripti:

cat > benchmark.js << 'EOF'
import init, { process_csv } from './pkg/csv_processor.js';

async function benchmark() {
    await init();
    
    // Test verisi oluştur
    function generateCSV(rows) {
        let csv = 'id,value,timestamp,categoryn';
        for (let i = 0; i < rows; i++) {
            csv += `${i},${Math.random() * 1000},${Date.now()},cat${i % 5}n`;
        }
        return csv;
    }
    
    const sizes = [10000, 50000, 100000, 500000];
    const results = [];
    
    for (const size of sizes) {
        const csv = generateCSV(size);
        const csvSizeMB = (new Blob([csv]).size / 1024 / 1024).toFixed(2);
        
        // WASM ile
        const wasmStart = performance.now();
        const wasmResult = process_csv(csv);
        const wasmTime = performance.now() - wasmStart;
        
        results.push({
            rows: size,
            size_mb: csvSizeMB,
            wasm_ms: wasmTime.toFixed(2),
            rows_per_second: Math.round(size / (wasmTime / 1000))
        });
        
        console.log(`${size} satır (${csvSizeMB}MB): ${wasmTime.toFixed(2)}ms`);
        console.log(`Throughput: ${Math.round(size / (wasmTime / 1000)).toLocaleString()} satır/saniye`);
    }
    
    return results;
}

benchmark().then(results => {
    console.table(results);
});
EOF

Pratik sonuçlar şöyle görünüyor: modern bir laptop tarayıcısında 100.000 satırlık CSV için WASM yaklaşık 150-200ms alıyor. Aynı işi pure JavaScript ile yaparsanız 1200-1800ms. Fark %6-8 civarında, küçük bir veri seti için belki önemsiz, ama 500.000 satırda bu fark çok daha belirgin hale geliyor.

Dikkat Edilmesi Gereken Noktalar

Üretim ortamında öğrendiğim bazı dersler var, bunları paylaşmadan olmaz:

  • WASM başlangıç maliyeti: init() fonksiyonu ilk çağrıda 50-200ms sürebilir. Bunu uygulama yüklenirken arka planda yapın, kullanıcı dosya seçmeden önce hazır olsun.
  • Bellek limitleri: Tarayıcılar WASM modüllerine genellikle 4GB bellek limiti koyuyor. Ama pratikte 1-2GB’ı geçmemek güvenli. Streaming yaklaşımı burada hayat kurtarıyor.
  • COOP/COEP zorunluluğu: SharedArrayBuffer ve bazı paralel işlemler için Cross-Origin-Opener-Policy ve Cross-Origin-Embedder-Policy header’larını ayarlamak şart. Nginx konfigürasyonunda bunu göstermiştim.
  • Firefox ve Safari uyumu: Temel WASM her majör tarayıcıda destekleniyor. Ama WASM Threads ve SIMD için destek matüritesi değişiyor. Önce temel WASM ile gidin, optimizasyonları sonra ekleyin.
  • Error handling: Rust panic’leri JavaScript exception’a dönüşür ama mesajlar her zaman anlamlı olmayabilir. Rust tarafında Result kullanın ve hataları düzgün propagate edin.
  • Bundle size: wasm-opt aracını kullanarak WASM binary’yi küçültebilirsiniz. wasm-pack build zaten bunu yapıyor release modda, ama wasm-opt -O3 ile manuel geçiş de deneyebilirsiniz.

Sonuç

WASM ile tarayıcı-taraflı büyük veri işleme, özellikle hassas veri barındıran uygulamalar için gerçek bir paradigma değişikliği. Sunucu maliyetlerini düşürüyor, gecikmeyi ortadan kaldırıyor ve kullanıcı deneyimini iyileştiriyor. Ama her çözüm gibi, doğru kullanım senaryosu önemli.

Eğer verileriniz hassas değil ve zaten sunucuya gidecekse, WASM eklemek gereksiz karmaşıklık getirebilir. Ama kullanıcının kendi verisini analiz ettiği, sonucun önemli olup ham verinin olmadığı senaryolarda, WASM gerçekten parlıyor.

Rust ekosistemi bu alanda oldukça olgunlaştı. wasm-pack, wasm-bindgen, serde-wasm-bindgen üçlüsüyle makul çabayla production-ready bir şey çıkarmak mümkün. Denemek için kendi log dosyalarınızı veya CSV exportlarınızı işleyen küçük bir araçla başlayın, farkı hissedeceksiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir