Network Packets
A network router simulation demonstrating multiple probability distributions working together in a realistic scenario.
What it demonstrates
- Five distributions used together: exponential, zipf, gaussian, bernoulli, uniform
- Standalone distribution factories initialized with the simulation’s PRNG
- Entity management for router state
- Statistics collection for traffic analysis
Distributions used
| Distribution | Purpose |
|---|---|
exponential(rate) | Inter-arrival times (Poisson process) |
zipf(n, s) | Packet size ranks (few large, many small) |
gaussian(mean, stddev) | Base processing time |
bernoulli(p) | Random packet drop decision |
uniform(min, max) | Jitter added to processing time |
Code
import { SimulationEngine } from 'simloop';
import type { SimContext } from 'simloop';
import { exponential, zipf, gaussian, bernoulli, uniform } from 'simloop';
const CONFIG = {
seed: 2024,
maxTime: 1000,
avgArrivalRate: 5,
packetSizeRanks: 5,
zipfExponent: 1.5,
meanProcessTime: 1.2,
processTimeStddev: 0.3,
dropProbability: 0.02,
jitterMin: 0.0,
jitterMax: 0.5,
};
const PACKET_SIZES = [64, 256, 512, 1024, 1500]; // bytes per rank
type NetworkEvents = {
'packet:arrive': { packetId: string; sizeBytes: number };
'packet:process': { packetId: string; sizeBytes: number };
'packet:done': { packetId: string; sizeBytes: number };
'packet:drop': { packetId: string; sizeBytes: number; reason: string };
};
interface RouterState {
queue: string[];
processing: boolean;
totalBytes: number;
}
const sim = new SimulationEngine<NetworkEvents>({
seed: CONFIG.seed,
maxTime: CONFIG.maxTime,
logLevel: 'info',
name: 'NetworkRouter',
});
// Distribution samplers (initialized in init)
let nextArrival: () => number;
let packetSizeRank: () => number;
let processTime: () => number;
let shouldDrop: () => number;
let jitter: () => number;
sim.on('packet:arrive', (event, ctx) => {
const { packetId, sizeBytes } = event.payload;
const router = ctx.getEntity<RouterState>('router')!;
ctx.stats.increment('arrivals');
ctx.stats.record('packetSize', sizeBytes);
if (shouldDrop() === 1) {
ctx.stats.increment('dropped');
ctx.schedule('packet:drop', ctx.clock, { packetId, sizeBytes, reason: 'random' });
} else {
router.state.queue.push(packetId);
if (!router.state.processing) {
tryProcessNext(ctx);
}
}
// Schedule next arrival
const arrivalDelay = nextArrival();
const rank = packetSizeRank();
const nextSize = PACKET_SIZES[rank - 1];
const nextId = `pkt-${ctx.stats.get('arrivals').count + 1}`;
ctx.schedule('packet:arrive', ctx.clock + arrivalDelay, {
packetId: nextId,
sizeBytes: nextSize,
});
});
sim.on('packet:process', (event, ctx) => {
const router = ctx.getEntity<RouterState>('router')!;
router.state.processing = true;
const baseTime = Math.max(0.1, processTime());
const addedJitter = jitter();
const totalTime = baseTime + addedJitter;
ctx.stats.record('processTime', totalTime);
ctx.schedule('packet:done', ctx.clock + totalTime, {
packetId: event.payload.packetId,
sizeBytes: event.payload.sizeBytes,
});
});
sim.on('packet:done', (event, ctx) => {
const router = ctx.getEntity<RouterState>('router')!;
router.state.processing = false;
router.state.totalBytes += event.payload.sizeBytes;
ctx.stats.increment('processed');
ctx.stats.record('throughputBytes', event.payload.sizeBytes);
tryProcessNext(ctx);
});
sim.on('packet:drop', () => {
// Marker event for tracing; stats already recorded
});
function tryProcessNext(ctx: SimContext<NetworkEvents>): void {
const router = ctx.getEntity<RouterState>('router')!;
if (router.state.queue.length === 0 || router.state.processing) return;
const packetId = router.state.queue.shift()!;
const rank = packetSizeRank();
const sizeBytes = PACKET_SIZES[rank - 1];
ctx.schedule('packet:process', ctx.clock, { packetId, sizeBytes });
}
sim.init((ctx) => {
// Initialize samplers with simulation's PRNG
const rng = () => ctx.random();
nextArrival = exponential(rng, CONFIG.avgArrivalRate);
packetSizeRank = zipf(rng, CONFIG.packetSizeRanks, CONFIG.zipfExponent);
processTime = gaussian(rng, CONFIG.meanProcessTime, CONFIG.processTimeStddev);
shouldDrop = bernoulli(rng, CONFIG.dropProbability);
jitter = uniform(rng, CONFIG.jitterMin, CONFIG.jitterMax);
ctx.addEntity<RouterState>({
id: 'router',
state: { queue: [], processing: false, totalBytes: 0 },
});
const firstRank = packetSizeRank();
ctx.schedule('packet:arrive', 0, {
packetId: 'pkt-1',
sizeBytes: PACKET_SIZES[firstRank - 1],
});
});
sim.onEnd((ctx) => {
const arrivals = ctx.stats.get('arrivals');
const processed = ctx.stats.get('processed');
const dropped = ctx.stats.get('dropped');
const procTime = ctx.stats.get('processTime');
const router = ctx.getEntity<RouterState>('router')!;
console.log(`Arrivals: ${arrivals.count}, Processed: ${processed.count}`);
console.log(`Dropped: ${dropped.count} (${((dropped.count / arrivals.count) * 100).toFixed(1)}%)`);
console.log(`Throughput: ${(router.state.totalBytes / 1024).toFixed(1)} KB`);
console.log(`Avg process time: ${procTime.mean.toFixed(3)}`);
});
const result = sim.run();Key takeaways
- Distribution factories are standalone — they can be initialized outside handlers using any
() => numberRNG source - The
rngfunction wrapsctx.random()to maintain determinism through the simulation’s seeded PRNG - The router processes one packet at a time; if the queue is non-empty after completion, the next packet starts immediately
Last updated on