Browse Source

fix simulation for url shortener, remove unused properties

main
Stephanie Gredell 4 months ago
parent
commit
1623c55fe0
  1. 1
      .gitignore
  2. 10
      data/levels.json
  3. 16
      internal/simulation/cache.go
  4. 8
      internal/simulation/engine.go
  5. 127
      internal/simulation/microservice.go
  6. 18
      internal/simulation/user.go
  7. 20
      internal/simulation/webserver.go
  8. 5
      router/handlers/chat.go
  9. 4
      router/handlers/game.go
  10. 62
      router/handlers/simulation.go
  11. 61
      static/app.js
  12. 39
      static/canvas.html
  13. 5
      static/commands.js
  14. 52
      static/index.html
  15. 4
      static/plugins/microservice.js
  16. 4
      static/plugins/webserver.js
  17. 1
      static/states/CanvasStateMachine.js

1
.gitignore vendored

@ -1,2 +1,3 @@
.env .env
/tmp /tmp
systemdesigngame

10
data/levels.json

@ -5,7 +5,6 @@
"description": "Scale your URL shortener to handle traffic spikes and ensure high availability.", "description": "Scale your URL shortener to handle traffic spikes and ensure high availability.",
"targetRps": 1000, "targetRps": 1000,
"durationSec": 180, "durationSec": 180,
"maxMonthlyUsd": 300,
"maxP95LatencyMs": 150, "maxP95LatencyMs": 150,
"requiredAvailabilityPct": 99.9, "requiredAvailabilityPct": 99.9,
"mustInclude": ["database", "loadBalancer"], "mustInclude": ["database", "loadBalancer"],
@ -24,7 +23,6 @@
"Target RPS: 1000", "Target RPS: 1000",
"Max P95 latency: 150ms", "Max P95 latency: 150ms",
"Required availability: 99.9%", "Required availability: 99.9%",
"Max monthly cost: $300",
"Simulation duration: 180 seconds" "Simulation duration: 180 seconds"
] ]
}, },
@ -34,7 +32,6 @@
"description": "Support real-time chat across mobile and web, with message persistence.", "description": "Support real-time chat across mobile and web, with message persistence.",
"targetRps": 500, "targetRps": 500,
"durationSec": 300, "durationSec": 300,
"maxMonthlyUsd": 500,
"maxP95LatencyMs": 200, "maxP95LatencyMs": 200,
"requiredAvailabilityPct": 99.9, "requiredAvailabilityPct": 99.9,
"mustInclude": ["webserver", "database", "messageQueue"], "mustInclude": ["webserver", "database", "messageQueue"],
@ -53,7 +50,6 @@
"Target RPS: 500", "Target RPS: 500",
"Max P95 latency: 200ms", "Max P95 latency: 200ms",
"Required availability: 99.9%", "Required availability: 99.9%",
"Max monthly cost: $500",
"Simulation duration: 300 seconds" "Simulation duration: 300 seconds"
] ]
}, },
@ -63,7 +59,6 @@
"description": "Add video transcoding, caching, and recommendations.", "description": "Add video transcoding, caching, and recommendations.",
"targetRps": 1000, "targetRps": 1000,
"durationSec": 600, "durationSec": 600,
"maxMonthlyUsd": 2000,
"maxP95LatencyMs": 300, "maxP95LatencyMs": 300,
"requiredAvailabilityPct": 99.9, "requiredAvailabilityPct": 99.9,
"mustInclude": ["cdn", "data pipeline", "cache"], "mustInclude": ["cdn", "data pipeline", "cache"],
@ -82,7 +77,6 @@
"Target RPS: 1000", "Target RPS: 1000",
"Max P95 latency: 300ms", "Max P95 latency: 300ms",
"Required availability: 99.9%", "Required availability: 99.9%",
"Max monthly cost: $2000",
"Simulation duration: 600 seconds" "Simulation duration: 600 seconds"
] ]
}, },
@ -92,7 +86,6 @@
"description": "Design a rate limiter that works across multiple instances and enforces global quotas.", "description": "Design a rate limiter that works across multiple instances and enforces global quotas.",
"targetRps": 1000, "targetRps": 1000,
"durationSec": 180, "durationSec": 180,
"maxMonthlyUsd": 300,
"maxP95LatencyMs": 50, "maxP95LatencyMs": 50,
"requiredAvailabilityPct": 99.9, "requiredAvailabilityPct": 99.9,
"mustInclude": ["webserver", "cache"], "mustInclude": ["webserver", "cache"],
@ -112,7 +105,6 @@
"Target RPS: 1000", "Target RPS: 1000",
"Max P95 latency: 50ms", "Max P95 latency: 50ms",
"Required availability: 99.9%", "Required availability: 99.9%",
"Max monthly cost: $300",
"Simulation duration: 180 seconds" "Simulation duration: 180 seconds"
] ]
}, },
@ -122,7 +114,6 @@
"description": "Design a pull-based metrics system like Prometheus that scrapes multiple services.", "description": "Design a pull-based metrics system like Prometheus that scrapes multiple services.",
"targetRps": 1000, "targetRps": 1000,
"durationSec": 300, "durationSec": 300,
"maxMonthlyUsd": 500,
"maxP95LatencyMs": 100, "maxP95LatencyMs": 100,
"requiredAvailabilityPct": 99.9, "requiredAvailabilityPct": 99.9,
"mustInclude": ["data pipeline", "monitoring/alerting"], "mustInclude": ["data pipeline", "monitoring/alerting"],
@ -142,7 +133,6 @@
"Target RPS: 1000", "Target RPS: 1000",
"Max P95 latency: 100ms", "Max P95 latency: 100ms",
"Required availability: 99.9%", "Required availability: 99.9%",
"Max monthly cost: $500",
"Simulation duration: 300 seconds" "Simulation duration: 300 seconds"
] ]
} }

16
internal/simulation/cache.go

@ -1,11 +1,20 @@
package simulation package simulation
import ( import (
"fmt"
"hash/fnv"
"time" "time"
) )
type CacheLogic struct{} type CacheLogic struct{}
// hash function to simulate URL patterns
func hash(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}
type CacheEntry struct { type CacheEntry struct {
Data string Data string
Timestamp int Timestamp int
@ -52,12 +61,16 @@ func (c CacheLogic) Tick(props map[string]any, queue []*Request, tick int) ([]*R
output := []*Request{} output := []*Request{}
for _, req := range queue { for _, req := range queue {
cacheKey := req.ID + "-" + req.Type // Use request ID and type as cache key // For URL shortener simulation, use hash of request ID to simulate repeated URL access
// This creates realistic cache patterns where some URLs are accessed multiple times
hashValue := hash(req.ID) % 100 // Create 100 possible "URLs"
cacheKey := fmt.Sprintf("url-%d-%s", hashValue, req.Type)
// Check for cache hit // Check for cache hit
entry, hit := cacheData[cacheKey] entry, hit := cacheData[cacheKey]
if hit && !c.isExpired(entry, currentTime, cacheTTL) { if hit && !c.isExpired(entry, currentTime, cacheTTL) {
// Cache hit - return immediately with minimal latency // Cache hit - return immediately with minimal latency
// Cache hit - served from cache component
reqCopy := *req reqCopy := *req
reqCopy.LatencyMS += 1 // 1ms for in-memory access reqCopy.LatencyMS += 1 // 1ms for in-memory access
reqCopy.Path = append(reqCopy.Path, "cache-hit") reqCopy.Path = append(reqCopy.Path, "cache-hit")
@ -69,6 +82,7 @@ func (c CacheLogic) Tick(props map[string]any, queue []*Request, tick int) ([]*R
output = append(output, &reqCopy) output = append(output, &reqCopy)
} else { } else {
// Cache miss - forward request downstream // Cache miss - forward request downstream
// Cache miss - forwarding to database
reqCopy := *req reqCopy := *req
reqCopy.Path = append(reqCopy.Path, "cache-miss") reqCopy.Path = append(reqCopy.Path, "cache-miss")

8
internal/simulation/engine.go

@ -33,6 +33,8 @@ type Request struct {
Type string Type string
// records where it's been (used to prevent loops) // records where it's been (used to prevent loops)
Path []string Path []string
// cache key for cache-aside pattern (used by microservices)
CacheKey string
} }
// what hte system looks like given a tick // what hte system looks like given a tick
@ -128,7 +130,7 @@ func (e *SimulationEngine) Run(duration int, tickMs int) []*TickSnapshot {
} }
// this will preopulate some props so that we can use different load balancing algorithms // this will preopulate some props so that we can use different load balancing algorithms
if node.Type == "loadbalancer" { if node.Type == "loadbalancer" || node.Type == "loadBalancer" {
targets := e.Edges[id] targets := e.Edges[id]
node.Props["_numTargets"] = float64(len(targets)) node.Props["_numTargets"] = float64(len(targets))
node.Props["_targetIDs"] = targets node.Props["_targetIDs"] = targets
@ -179,9 +181,11 @@ func (e *SimulationEngine) Run(duration int, tickMs int) []*TickSnapshot {
func GetLogicForType(t string) NodeLogic { func GetLogicForType(t string) NodeLogic {
switch t { switch t {
case "user":
return UserLogic{}
case "webserver": case "webserver":
return WebServerLogic{} return WebServerLogic{}
case "loadbalancer": case "loadBalancer":
return LoadBalancerLogic{} return LoadBalancerLogic{}
case "cdn": case "cdn":
return CDNLogic{} return CDNLogic{}

127
internal/simulation/microservice.go

@ -1,6 +1,10 @@
package simulation package simulation
import "math" import (
"fmt"
"hash/fnv"
"math"
)
type MicroserviceLogic struct{} type MicroserviceLogic struct{}
@ -10,6 +14,21 @@ type ServiceInstance struct {
HealthStatus string HealthStatus string
} }
// CacheEntry represents a cached item in the microservice's cache
type MicroserviceCacheEntry struct {
Data string
Timestamp int
AccessTime int
AccessCount int
}
// hash function for cache keys
func hashKey(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}
func (m MicroserviceLogic) Tick(props map[string]any, queue []*Request, tick int) ([]*Request, bool) { func (m MicroserviceLogic) Tick(props map[string]any, queue []*Request, tick int) ([]*Request, bool) {
// Extract microservice properties // Extract microservice properties
instanceCount := int(AsFloat64(props["instanceCount"])) instanceCount := int(AsFloat64(props["instanceCount"]))
@ -56,36 +75,91 @@ func (m MicroserviceLogic) Tick(props map[string]any, queue []*Request, tick int
toProcess = queue[:totalCapacity] toProcess = queue[:totalCapacity]
} }
output := []*Request{} // Initialize cache in microservice props
cache, ok := props["_microserviceCache"].(map[string]*MicroserviceCacheEntry)
// Distribute requests across instances using round-robin if !ok {
for i, req := range toProcess { cache = make(map[string]*MicroserviceCacheEntry)
props["_microserviceCache"] = cache
}
// Create processed request copy cacheTTL := 300000 // 5 minutes default TTL
reqCopy := *req currentTime := tick * 100 // assuming 100ms per tick
// Add microservice processing latency output := []*Request{} // Only cache misses go here (forwarded to database)
processingLatency := baseLatencyMs cacheHits := []*Request{} // Cache hits - completed locally
dbRequests := []*Request{} // Requests that need to go to database
// Simulate CPU-bound vs I/O-bound operations // Process each request with cache-aside logic
if req.Type == "GET" { for i, req := range toProcess {
processingLatency = baseLatencyMs // Fast reads // Generate cache key for this request (simulate URL patterns)
} else if req.Type == "POST" || req.Type == "PUT" { hashValue := hashKey(req.ID) % 100 // Create 100 possible "URLs"
processingLatency = baseLatencyMs + 10 // Writes take longer cacheKey := fmt.Sprintf("url-%d-%s", hashValue, req.Type)
} else if req.Type == "COMPUTE" {
processingLatency = baseLatencyMs + 50 // CPU-intensive operations // Check cache first (Cache-Aside pattern)
entry, hit := cache[cacheKey]
if hit && !m.isCacheExpired(entry, currentTime, cacheTTL) {
// CACHE HIT - serve from cache (NO DATABASE QUERY)
reqCopy := *req
reqCopy.LatencyMS += 1 // 1ms for cache access
reqCopy.Path = append(reqCopy.Path, "microservice-cache-hit-completed")
// Update cache access tracking
entry.AccessTime = currentTime
entry.AccessCount++
// Cache hits do NOT go to database - they complete here
// In a real system, this response would go back to the client
// Store separately - these do NOT get forwarded to database
cacheHits = append(cacheHits, &reqCopy)
} else {
// CACHE MISS - need to query database
reqCopy := *req
// Add microservice processing latency
processingLatency := baseLatencyMs
// Simulate CPU-bound vs I/O-bound operations
if req.Type == "GET" {
processingLatency = baseLatencyMs // Fast reads
} else if req.Type == "POST" || req.Type == "PUT" {
processingLatency = baseLatencyMs + 10 // Writes take longer
} else if req.Type == "COMPUTE" {
processingLatency = baseLatencyMs + 50 // CPU-intensive operations
}
// Instance load affects latency (queuing delay)
instanceLoad := m.calculateInstanceLoad(i, len(toProcess), instanceCount)
if float64(instanceLoad) > float64(rpsCapacity)*0.8 { // Above 80% capacity
processingLatency += int(float64(processingLatency) * 0.5) // 50% penalty
}
reqCopy.LatencyMS += processingLatency
reqCopy.Path = append(reqCopy.Path, "microservice-cache-miss")
// Store cache key in request for when database response comes back
reqCopy.CacheKey = cacheKey
// Forward to database for actual data
dbRequests = append(dbRequests, &reqCopy)
} }
}
// Instance load affects latency (queuing delay) // For cache misses, we would normally wait for database response and then cache it
instanceLoad := m.calculateInstanceLoad(i, len(toProcess), instanceCount) // In this simulation, we'll immediately cache the "result" for future requests
if float64(instanceLoad) > float64(rpsCapacity)*0.8 { // Above 80% capacity for _, req := range dbRequests {
processingLatency += int(float64(processingLatency) * 0.5) // 50% penalty // Simulate caching the database response
cache[req.CacheKey] = &MicroserviceCacheEntry{
Data: "cached-response-data",
Timestamp: currentTime,
AccessTime: currentTime,
AccessCount: 1,
} }
reqCopy.LatencyMS += processingLatency // Forward request to database
reqCopy.Path = append(reqCopy.Path, "microservice-processed") output = append(output, req)
output = append(output, &reqCopy)
} }
// Health check: service is healthy if not severely overloaded // Health check: service is healthy if not severely overloaded
@ -94,6 +168,11 @@ func (m MicroserviceLogic) Tick(props map[string]any, queue []*Request, tick int
return output, healthy return output, healthy
} }
// isCacheExpired checks if a cache entry has expired
func (m MicroserviceLogic) isCacheExpired(entry *MicroserviceCacheEntry, currentTime, ttl int) bool {
return (currentTime - entry.Timestamp) > ttl
}
// calculateBaseLatency determines base processing time based on resources // calculateBaseLatency determines base processing time based on resources
func (m MicroserviceLogic) calculateBaseLatency(cpu, ramGb int) int { func (m MicroserviceLogic) calculateBaseLatency(cpu, ramGb int) int {
// Better CPU and RAM = lower base latency // Better CPU and RAM = lower base latency

18
internal/simulation/user.go

@ -0,0 +1,18 @@
package simulation
// UserLogic represents the behavior of user components in the simulation.
// User components serve as traffic sources and don't process requests themselves.
// Traffic generation is handled by the simulation engine at the entry point.
type UserLogic struct{}
// Tick implements the NodeLogic interface for User components.
// User components don't process requests - they just pass them through.
// The simulation engine handles traffic generation at entry points.
func (u UserLogic) Tick(props map[string]any, queue []*Request, tick int) ([]*Request, bool) {
// User components just pass through any requests they receive
// In practice, User components are typically entry points so they
// receive requests from the simulation engine itself
return queue, true
}

20
internal/simulation/webserver.go

@ -13,14 +13,22 @@ func (l WebServerLogic) Tick(props map[string]any, queue []*Request, tick int) (
toProcess = queue[:maxRPS] toProcess = queue[:maxRPS]
} }
// Get base latency for web server operations
baseLatencyMs := int(AsFloat64(props["baseLatencyMs"]))
if baseLatencyMs == 0 {
baseLatencyMs = 20 // default 20ms for web server processing
}
var output []*Request var output []*Request
for _, req := range toProcess { for _, req := range toProcess {
output = append(output, &Request{ // Create a copy of the request to preserve existing latency
ID: req.ID, reqCopy := *req
Timestamp: req.Timestamp,
Origin: req.Origin, // Add web server processing latency
Type: req.Type, reqCopy.LatencyMS += baseLatencyMs
}) reqCopy.Path = append(reqCopy.Path, "webserver-processed")
output = append(output, &reqCopy)
} }
return output, true return output, true

5
router/handlers/chat.go

@ -42,8 +42,6 @@ func Messages(w http.ResponseWriter, r *http.Request) {
break break
} }
fmt.Printf("message: %s", message)
var messageReceived MessageReceived var messageReceived MessageReceived
err = json.Unmarshal(message, &messageReceived) err = json.Unmarshal(message, &messageReceived)
if err != nil { if err != nil {
@ -53,9 +51,8 @@ func Messages(w http.ResponseWriter, r *http.Request) {
if messageReceived.Message == "" { if messageReceived.Message == "" {
messageReceived.Message = "<user did not send text>" messageReceived.Message = "<user did not send text>"
} else {
messageReceived.Message = string(message)
} }
// Note: messageReceived.Message is already properly parsed from JSON, no need to overwrite it
prompt := fmt.Sprintf("You are a tutor that helps people learn system design. You will be given a JSON payload that looks like %s. The nodes are the components a user can put into their design and the connections will tell you how they are connected. The level name identifies what problem they are working on as well as a difficulty level. Each level has an easy, medium or hard setting. Also in the payload, there is a list of components that a user can use to build their design. Your hints and responses should only refer to these components and not refer to things that the user cannot use. Always refer to the nodes by their type. Please craft your response as if you're talking to the user. And do not reference the payload as \"payload\" but as their design. Also, please do not show the payload in your response. Do not refer to components as node-0 or whatever. Always refer to the type of component they are. Always assume that the source of traffic for any system is a user. The user component will not be visible in teh payload. Also make sure you use html to format your answer. Do not over format your response. Only use p tags. Format lists using proper lists html. Anytime the user sends a different payload back to you, make note of what is correct. Never give the actual answer, only helpful hints. If the available components do not allow the user to feasibly solve the system design problem, you should mention it and then tell them what exactly is missing from the list.", messageReceived.DesignPayload) prompt := fmt.Sprintf("You are a tutor that helps people learn system design. You will be given a JSON payload that looks like %s. The nodes are the components a user can put into their design and the connections will tell you how they are connected. The level name identifies what problem they are working on as well as a difficulty level. Each level has an easy, medium or hard setting. Also in the payload, there is a list of components that a user can use to build their design. Your hints and responses should only refer to these components and not refer to things that the user cannot use. Always refer to the nodes by their type. Please craft your response as if you're talking to the user. And do not reference the payload as \"payload\" but as their design. Also, please do not show the payload in your response. Do not refer to components as node-0 or whatever. Always refer to the type of component they are. Always assume that the source of traffic for any system is a user. The user component will not be visible in teh payload. Also make sure you use html to format your answer. Do not over format your response. Only use p tags. Format lists using proper lists html. Anytime the user sends a different payload back to you, make note of what is correct. Never give the actual answer, only helpful hints. If the available components do not allow the user to feasibly solve the system design problem, you should mention it and then tell them what exactly is missing from the list.", messageReceived.DesignPayload)

4
router/handlers/game.go

@ -3,7 +3,6 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html"
"html/template" "html/template"
"net/http" "net/http"
"systemdesigngame/internal/auth" "systemdesigngame/internal/auth"
@ -26,8 +25,7 @@ func (h *PlayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
levelPayload, err := json.Marshal(lvl) levelPayload, err := json.Marshal(lvl)
unescapedHtml := html.UnescapeString(string(levelPayload))
fmt.Printf("raw message: %v", string(json.RawMessage(unescapedHtml)))
if err != nil { if err != nil {
fmt.Printf("error marshaling level: %v", err) fmt.Printf("error marshaling level: %v", err)
} }

62
router/handlers/simulation.go

@ -15,7 +15,7 @@ type SimulationResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Metrics map[string]interface{} `json:"metrics,omitempty"` Metrics map[string]interface{} `json:"metrics,omitempty"`
Timeline []interface{} `json:"timeline,omitempty"` Timeline []interface{} `json:"timeline,omitempty"`
Passed bool `json:"passed,omitempty"` Passed bool `json:"passed"`
Score int `json:"score,omitempty"` Score int `json:"score,omitempty"`
Feedback []string `json:"feedback,omitempty"` Feedback []string `json:"feedback,omitempty"`
LevelName string `json:"levelName,omitempty"` LevelName string `json:"levelName,omitempty"`
@ -60,7 +60,16 @@ func (h *SimulationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
// Set simulation parameters // Set simulation parameters
engine.RPS = 50 // Default RPS - could be configurable later defaultRPS := 50
targetRPS := defaultRPS
if requestBody.LevelID != "" {
if lvl, err := level.GetLevelByID(requestBody.LevelID); err == nil {
targetRPS = lvl.TargetRPS
}
}
engine.RPS = targetRPS
// Find entry node by analyzing topology // Find entry node by analyzing topology
entryNode := findEntryNode(design) entryNode := findEntryNode(design)
@ -81,7 +90,7 @@ func (h *SimulationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
snapshots := engine.Run(60, 100) snapshots := engine.Run(60, 100)
// Calculate metrics from snapshots // Calculate metrics from snapshots
metrics := calculateMetrics(snapshots) metrics := calculateMetrics(snapshots, design)
// Convert snapshots to interface{} for JSON serialization // Convert snapshots to interface{} for JSON serialization
timeline := make([]interface{}, len(snapshots)) timeline := make([]interface{}, len(snapshots))
@ -122,7 +131,7 @@ func (h *SimulationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
// calculateMetrics computes key performance metrics from simulation snapshots // calculateMetrics computes key performance metrics from simulation snapshots
func calculateMetrics(snapshots []*simulation.TickSnapshot) map[string]interface{} { func calculateMetrics(snapshots []*simulation.TickSnapshot, design design.Design) map[string]interface{} {
if len(snapshots) == 0 { if len(snapshots) == 0 {
return map[string]interface{}{ return map[string]interface{}{
"throughput": 0, "throughput": 0,
@ -173,12 +182,12 @@ func calculateMetrics(snapshots []*simulation.TickSnapshot) map[string]interface
availability = (float64(totalHealthy) / float64(totalNodes)) * 100 availability = (float64(totalHealthy) / float64(totalNodes)) * 100
} }
// Estimate monthly cost (placeholder - could be enhanced) // Calculate monthly cost based on component specifications
monthlyCost := float64(totalNodes) * 50 // $50 per node per month baseline monthlyCost := calculateRealMonthlyCost(design.Nodes)
return map[string]interface{}{ return map[string]interface{}{
"throughput": int(throughput), "throughput": int(throughput),
"latency_avg": int(avgLatency), "latency_avg": avgLatency,
"cost_monthly": int(monthlyCost), "cost_monthly": int(monthlyCost),
"availability": availability, "availability": availability,
} }
@ -264,19 +273,10 @@ func validateLevel(lvl *level.Level, design design.Design, metrics map[string]in
var passedRequirements []string var passedRequirements []string
// Extract metrics // Extract metrics
throughput := metrics["throughput"].(int) avgLatency := int(metrics["latency_avg"].(float64))
avgLatency := metrics["latency_avg"].(int)
availability := metrics["availability"].(float64) availability := metrics["availability"].(float64)
monthlyCost := metrics["cost_monthly"].(int) monthlyCost := metrics["cost_monthly"].(int)
// Check throughput requirement
if throughput >= lvl.TargetRPS {
passedRequirements = append(passedRequirements, "Throughput requirement met")
} else {
failedRequirements = append(failedRequirements,
fmt.Sprintf("Throughput: %d RPS (required: %d RPS)", throughput, lvl.TargetRPS))
}
// Check latency requirement (using avg latency as approximation for P95) // Check latency requirement (using avg latency as approximation for P95)
if avgLatency <= lvl.MaxP95LatencyMs { if avgLatency <= lvl.MaxP95LatencyMs {
passedRequirements = append(passedRequirements, "Latency requirement met") passedRequirements = append(passedRequirements, "Latency requirement met")
@ -422,6 +422,34 @@ func calculateScore(passedCount, failedCount int, metrics map[string]interface{}
return min(100, baseScore+performanceBonus) return min(100, baseScore+performanceBonus)
} }
// calculateRealMonthlyCost computes monthly cost based on actual component specifications
func calculateRealMonthlyCost(nodes []design.Node) float64 {
totalCost := 0.0
for _, node := range nodes {
switch node.Type {
case "user":
// User components don't cost anything
continue
case "microservice":
if monthlyUsd, ok := node.Props["monthlyUsd"].(float64); ok {
if instanceCount, ok := node.Props["instanceCount"].(float64); ok {
totalCost += monthlyUsd * instanceCount
}
}
case "webserver":
if monthlyCost, ok := node.Props["monthlyCostUsd"].(float64); ok {
totalCost += monthlyCost
}
default:
// Default cost for other components (cache, database, load balancer, etc.)
totalCost += 20 // $20/month baseline
}
}
return totalCost
}
// Helper function // Helper function
func min(a, b int) int { func min(a, b int) int {
if a < b { if a < b {

61
static/app.js

@ -67,7 +67,6 @@ export class CanvasApp {
this.learnMoreBtn = document.getElementById('learn-more-button'); this.learnMoreBtn = document.getElementById('learn-more-button');
this.tabs = document.getElementsByClassName('tabinput'); this.tabs = document.getElementsByClassName('tabinput');
console.log(this.tabs)
this._reconnectDelay = 1000; this._reconnectDelay = 1000;
this._maxReconnectDelay = 15000; this._maxReconnectDelay = 15000;
this._reconnectTimer = null; this._reconnectTimer = null;
@ -168,7 +167,6 @@ export class CanvasApp {
exportDesign() { exportDesign() {
const nodes = this.placedComponents const nodes = this.placedComponents
.filter(n => n.type !== 'user')
.map(n => { .map(n => {
const plugin = PluginRegistry.get(n.type); const plugin = PluginRegistry.get(n.type);
const result = { const result = {
@ -198,8 +196,8 @@ export class CanvasApp {
return { return {
nodes, nodes,
connections, connections,
level: JSON.parse(this.level), level: this.level,
availableComponents: JSON.stringify(this.plugins) availableComponents: this.plugins
}; };
} }
@ -216,17 +214,17 @@ export class CanvasApp {
} }
showResults(result) { showResults(result) {
const metrics = result.Metrics; const metrics = result.metrics;
let message = ''; let message = '';
// Level validation results // Level validation results
if (result.LevelName) { if (result.levelName) {
if (result.Passed) { if (result.passed) {
message += `Level "${result.LevelName}" PASSED!\n`; message += `Level "${result.levelName}" PASSED!\n`;
message += `Score: ${result.Score}/100\n\n`; message += `Score: ${result.score}/100\n\n`;
} else { } else {
message += `Level "${result.LevelName}" FAILED\n`; message += `Level "${result.levelName}" FAILED\n`;
message += `Score: ${result.Score}/100\n\n`; message += `Score: ${result.score}/100\n\n`;
} }
// Add detailed feedback // Add detailed feedback
@ -243,7 +241,7 @@ export class CanvasApp {
message += `• Avg Latency: ${metrics.latency_avg}ms\n`; message += `• Avg Latency: ${metrics.latency_avg}ms\n`;
message += `• Availability: ${metrics.availability.toFixed(1)}%\n`; message += `• Availability: ${metrics.availability.toFixed(1)}%\n`;
message += `• Monthly Cost: $${metrics.cost_monthly}\n\n`; message += `• Monthly Cost: $${metrics.cost_monthly}\n\n`;
message += `Timeline: ${result.Timeline.length} ticks simulated`; message += `Timeline: ${result.timeline.length} ticks simulated`;
alert(message); alert(message);
@ -255,6 +253,43 @@ export class CanvasApp {
alert(`Simulation Error:\n\n${errorMessage}\n\nPlease check your design and try again.`); alert(`Simulation Error:\n\n${errorMessage}\n\nPlease check your design and try again.`);
} }
_initWebSocket() {
const scheme = location.protocol === "https:" ? "wss://" : "ws://";
this.ws = new WebSocket(scheme + location.host + "/ws");
this.ws.onopen = () => {
console.log("WebSocket connected");
// Reset reconnection delay on successful connection
this._reconnectDelay = 1000;
this.ws.send(JSON.stringify({
'designPayload': JSON.stringify(this.exportDesign()),
'message': ''
}));
};
this.ws.onmessage = (e) => {
this.chatLoadingIndicator.style.display = 'none';
this.chatTextField.disabled = false;
this.chatTextField.focus();
const message = document.createElement('p');
message.innerHTML = e.data;
message.className = "other";
this.chatMessages.insertBefore(message, this.chatLoadingIndicator);
};
this.ws.onerror = (err) => {
console.log("ws error:", err);
this._scheduleReconnect();
};
this.ws.onclose = () => {
console.log("WebSocket closed, scheduling reconnect...");
this.ws = null;
this._scheduleReconnect();
};
}
_scheduleReconnect() { _scheduleReconnect() {
if (this._stopped) return; if (this._stopped) return;
@ -265,7 +300,7 @@ export class CanvasApp {
const jitter = this._reconnectDelay * (Math.random() * 0.4 - 0.2); const jitter = this._reconnectDelay * (Math.random() * 0.4 - 0.2);
const delay = Math.max(250, Math.min(this._maxReconnectDelay, this._reconnectDelay + jitter)); const delay = Math.max(250, Math.min(this._maxReconnectDelay, this._reconnectDelay + jitter));
console.log(`Reconnecting websocket...`) console.log(`Reconnecting websocket in ${delay}ms...`)
this._reconnectTimer = setTimeout(() => { this._reconnectTimer = setTimeout(() => {
this._reconnectTimer = null; this._reconnectTimer = null;

39
static/canvas.html

@ -112,16 +112,8 @@
<select id="connection-protocol"> <select id="connection-protocol">
<option>HTTP</option> <option>HTTP</option>
<option>HTTPS</option> <option>HTTPS</option>
<option>gRPC</option> <option>Database</option>
<option>WebSocket</option>
<option>GraphQL</option>
<option>Kafka</option>
<option>AMQP</option>
<option>MQTT</option>
<option>SQL</option>
<option>NoSQL</option>
<option>Redis</option> <option>Redis</option>
<option>TLS</option>
</select> </select>
</label> </label>
<label style="margin-top: 10px;"> <label style="margin-top: 10px;">
@ -146,8 +138,6 @@
id="constraint-rps">{{.Level.TargetRPS}}</span></div> id="constraint-rps">{{.Level.TargetRPS}}</span></div>
<div class="panel-metric"><span class="label"> max p95 latency:</span> <span <div class="panel-metric"><span class="label"> max p95 latency:</span> <span
id="constraint-latency">{{.Level.MaxP95LatencyMs}}ms</span></div> id="constraint-latency">{{.Level.MaxP95LatencyMs}}ms</span></div>
<div class="panel-metric"><span class="label">💸 max cost:</span> <span
id="constraint-cost">${{.Level.MaxMonthlyUSD}}</span></div>
<div class="panel-metric"><span class="label">🔒 availability:</span> <span <div class="panel-metric"><span class="label">🔒 availability:</span> <span
id="constraint-availability">{{printf "%.2f" .Level.RequiredAvailabilityPct}}%</span> id="constraint-availability">{{printf "%.2f" .Level.RequiredAvailabilityPct}}%</span>
</div> </div>
@ -180,30 +170,23 @@
<label>Max Entries: <input name="maxEntries" type="number" /></label> <label>Max Entries: <input name="maxEntries" type="number" /></label>
<label>Eviction Policy: <label>Eviction Policy:
<select name="evictionPolicy"> <select name="evictionPolicy">
<option value="LRU">LRU</option> <option value="LRU">LRU (Least Recently Used)</option>
<option value="LFU">LFU</option> <option value="LFU">LFU (Least Frequently Used)</option>
<option value="Random">Random</option>
</select> </select>
</label> </label>
</div> </div>
<div id="compute-group" data-group="compute-group" class="prop-group"> <div id="compute-group" data-group="compute-group" class="prop-group">
<label>CPU Cores:</label>
<input type="number" name="cpu" min="1" />
<label>RAM (GB):</label>
<input type="number" name="ramGb" min="1" />
<label>RPS Capacity:</label> <label>RPS Capacity:</label>
<input type="number" name="rpsCapacity" min="1" /> <input type="number" name="rpsCapacity" min="1" />
<label>Monthly Cost (USD):</label> <label>Base Latency (ms):</label>
<input type="number" name="monthlyCostUsd" min="0" /> <input type="number" name="baseLatencyMs" min="1" />
</div> </div>
<div id="lb-group" data-group="lb-group" class="prop-group"> <div id="lb-group" data-group="lb-group" class="prop-group">
<label>Algorithm</label> <label>Algorithm</label>
<select name="algorithm"> <select name="algorithm">
<option value="round-robin">Round Robin</option> <option value="round-robin">Round Robin</option>
<option value="least-connections">Least Connections</option> <option value="least-connection">Least Connection</option>
</select> </select>
</div> </div>
<div id="mq-group" data-group="mq-group" class="prop-group"> <div id="mq-group" data-group="mq-group" class="prop-group">
@ -265,11 +248,6 @@
<input type="number" name="rpsCapacity" value="150" min="1" /> <input type="number" name="rpsCapacity" value="150" min="1" />
</label> </label>
<label>
Monthly Cost (USD):
<input type="number" name="monthlyUsd" value="18" min="0" step="1" />
</label>
<label> <label>
Scaling Strategy: Scaling Strategy:
<select name="scalingStrategy"> <select name="scalingStrategy">
@ -277,11 +255,6 @@
<option value="manual">Manual</option> <option value="manual">Manual</option>
</select> </select>
</label> </label>
<label>
API Version:
<input type="text" name="apiVersion" value="v1" />
</label>
</div> </div>
<div id="datapipeline-group" data-group="pipeline-group" class="prop-group"> <div id="datapipeline-group" data-group="pipeline-group" class="prop-group">
<label>Batch Size</label> <label>Batch Size</label>

5
static/commands.js

@ -164,8 +164,6 @@ export class StartChatCommand extends Command {
app.ws.onclose = () => { app.ws.onclose = () => {
console.log("leaving chat..."); console.log("leaving chat...");
app.ws = null; app.ws = null;
app._sentJoin = false;
delete app.players[app.pageData.username];
app._scheduleReconnect(); app._scheduleReconnect();
}; };
} }
@ -289,7 +287,8 @@ export class RunSimulationCommand extends Command {
const result = await response.json(); const result = await response.json();
if (result.Success) { console.log('result', result);
if (result.passed && result.success) {
console.log('Simulation successful:', result); console.log('Simulation successful:', result);
app.showResults(result); app.showResults(result);
} else { } else {

52
static/index.html

@ -68,6 +68,31 @@
color: #00ff88; color: #00ff88;
} }
.header-actions {
display: flex;
align-items: center;
gap: 20px;
}
.login-button {
background: linear-gradient(135deg, #00ff88, #00cc6a);
color: #000;
padding: 8px 16px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.login-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 255, 136, 0.3);
color: #000;
}
.beta-badge { .beta-badge {
background: linear-gradient(45deg, #ff6b35, #f7931e); background: linear-gradient(45deg, #ff6b35, #f7931e);
color: white; color: white;
@ -678,6 +703,15 @@
.nav-links { .nav-links {
display: none; display: none;
} }
.header-actions {
gap: 10px;
}
.login-button {
padding: 6px 12px;
font-size: 0.8rem;
}
} }
.success-message { .success-message {
@ -766,7 +800,9 @@
<li><a href="#how">How It Works</a></li> <li><a href="#how">How It Works</a></li>
<li><a href="#faq">FAQ</a></li> <li><a href="#faq">FAQ</a></li>
</ul> </ul>
<div class="beta-badge">COMING SOON</div> <div class="header-actions">
<a href="/login" class="login-button">Login</a>
</div>
</nav> </nav>
</header> </header>
@ -780,18 +816,18 @@
</div> </div>
<div class="cta-form"> <div class="cta-form">
<h3>🚀 Get Early Access</h3> <h3>🚀 Get Updates</h3>
<form class="final-cta-form" id="finalBetaForm" action="https://gmail.us7.list-manage.com/subscribe/post?u=913ad95101d97bff0b1873301&amp;id=77dabc87db&amp;f_id=0070c9e4f0" method="post" target="_blank" novalidate> <form class="final-cta-form" id="finalBetaForm" action="https://gmail.us7.list-manage.com/subscribe/post?u=913ad95101d97bff0b1873301&amp;id=77dabc87db&amp;f_id=0070c9e4f0" method="post" target="_blank" novalidate>
<input type="email" name="EMAIL" id="mce-EMAIL" placeholder="Enter your email" required> <input type="email" name="EMAIL" id="mce-EMAIL" placeholder="Enter your email" required>
<div style="position: absolute; left: -5000px;" aria-hidden="true"> <div style="position: absolute; left: -5000px;" aria-hidden="true">
<input type="text" name="b_913ad95101d97bff0b1873301_77dabc87db" tabindex="-1" value=""> <input type="text" name="b_913ad95101d97bff0b1873301_77dabc87db" tabindex="-1" value="">
</div> </div>
<button type="submit">Join Waitlist</button> <button type="submit">Get Updates</button>
</form> </form>
<div class="success-message" id="successMessage"> <div class="success-message" id="successMessage">
✅ You're in! We'll notify you when beta access is available. ✅ You're in! We'll keep you updated on our progress.
</div> </div>
<p class="beta-info">🔥 Be the first to know when we launch</p> <p class="beta-info">🔥 Stay updated on our latest developments</p>
</div> </div>
</div> </div>
</div> </div>
@ -974,7 +1010,7 @@
<div class="faq-list"> <div class="faq-list">
<div class="faq-item"> <div class="faq-item">
<div class="faq-question">When will the beta be available?</div> <div class="faq-question">When will the beta be available?</div>
<div class="faq-answer">We're currently in development and aiming to launch the beta in the coming months. Sign up for the waitlist to be notified as soon as it's ready.</div> <div class="faq-answer">We're currently in development and aiming to launch in the coming months. Sign up for updates to be notified as soon as it's ready.</div>
</div> </div>
<div class="faq-item"> <div class="faq-item">
<div class="faq-question">Do I need to install anything to use the System Design Game?</div> <div class="faq-question">Do I need to install anything to use the System Design Game?</div>
@ -999,13 +1035,13 @@
<section class="final-cta"> <section class="final-cta">
<div class="container"> <div class="container">
<h2>Ready to Level Up Your System Design Skills?</h2> <h2>Ready to Level Up Your System Design Skills?</h2>
<p>Join the waitlist and be the first to know when our interactive browser-based platform launches.</p> <p>Get updates and be the first to know about our latest platform developments.</p>
<form class="final-cta-form" id="finalBetaForm" action="https://gmail.us7.list-manage.com/subscribe/post?u=913ad95101d97bff0b1873301&amp;id=77dabc87db&amp;f_id=0070c9e4f0" method="post" target="_blank" novalidate> <form class="final-cta-form" id="finalBetaForm" action="https://gmail.us7.list-manage.com/subscribe/post?u=913ad95101d97bff0b1873301&amp;id=77dabc87db&amp;f_id=0070c9e4f0" method="post" target="_blank" novalidate>
<input type="email" name="EMAIL" id="mce-EMAIL" placeholder="Enter your email" required> <input type="email" name="EMAIL" id="mce-EMAIL" placeholder="Enter your email" required>
<div style="position: absolute; left: -5000px;" aria-hidden="true"> <div style="position: absolute; left: -5000px;" aria-hidden="true">
<input type="text" name="b_913ad95101d97bff0b1873301_77dabc87db" tabindex="-1" value=""> <input type="text" name="b_913ad95101d97bff0b1873301_77dabc87db" tabindex="-1" value="">
</div> </div>
<button type="submit">Join Waitlist</button> <button type="submit">Get Updates</button>
</form> </form>
</div> </div>
</section> </section>

4
static/plugins/microservice.js

@ -9,8 +9,6 @@ PluginRegistry.register('microservice', {
{ name: 'cpu', type: 'number', default: 2, group: 'microservice-group' }, { name: 'cpu', type: 'number', default: 2, group: 'microservice-group' },
{ name: 'ramGb', type: 'number', default: 4, group: 'microservice-group' }, { name: 'ramGb', type: 'number', default: 4, group: 'microservice-group' },
{ name: 'rpsCapacity', type: 'number', default: 150, group: 'microservice-group' }, { name: 'rpsCapacity', type: 'number', default: 150, group: 'microservice-group' },
{ name: 'monthlyUsd', type: 'number', default: 18, group: 'microservice-group' }, { name: 'scalingStrategy', type: 'string', default: 'auto', group: 'microservice-group' }
{ name: 'scalingStrategy', type: 'string', default: 'auto', group: 'microservice-group' },
{ name: 'apiVersion', type: 'string', default: 'v1', group: 'microservice-group' }
] ]
}); });

4
static/plugins/webserver.js

@ -5,9 +5,7 @@ PluginRegistry.register('webserver', {
label: 'Web Server', label: 'Web Server',
props: [ props: [
{ name: 'label', type: 'string', default: 'Web Server', group: 'label-group' }, { name: 'label', type: 'string', default: 'Web Server', group: 'label-group' },
{ name: 'cpu', type: 'number', default: 2, group: 'compute-group' },
{ name: 'ramGb', type: 'number', default: 4, group: 'compute-group' },
{ name: 'rpsCapacity', type: 'number', default: 200, group: 'compute-group' }, { name: 'rpsCapacity', type: 'number', default: 200, group: 'compute-group' },
{ name: 'monthlyCostUsd', type: 'number', default: 20, group: 'compute-group' } { name: 'baseLatencyMs', type: 'number', default: 20, group: 'compute-group' }
] ]
}); });

1
static/states/CanvasStateMachine.js

@ -158,3 +158,4 @@ export class CanvasStateMachine {
this.changeState('design'); this.changeState('design');
} }
} }

Loading…
Cancel
Save