diff --git a/proto/api.proto b/proto/api.proto index 4a606ba6b6..3684cb420c 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -363,12 +363,29 @@ message ErrorOutcome { } // RPC RaidSim + +// ChartInput describes the input for the chart generation +// and all the data needed to run parallel sims. +// I've also done integers everywhere to avoid having to deal with type conversions +message ChartInput { + repeated int32 stats_to_compare = 100; + int32 lower_bound = 101; + int32 upper_bound = 102; + int32 step = 103; + int32 current_value = 104; + int32 current_value_other = 105; +} + message RaidSimRequest { string request_id = 5; Raid raid = 1; Encounter encounter = 2; SimOptions sim_options = 3; SimType type = 4; + + // extended RaidSimRequest with ChartInput to save myself + // from having to deal with new message + ChartInput chart_input = 1000; } // Result from running the raid sim. @@ -386,6 +403,16 @@ message RaidSimResult { ErrorOutcome error = 5; int32 iterations_done = 7; + + // I had to add ChartInput here as well. The sims finish in indeterministic order + // so we cannot guarantee that they will end up ordered in a sensible way + // including the ChartInput lets us correlate the input and output + ChartInput chart_input = 1000; +} + +// ChartResult is a new response type for returning the aggregated sim results +message ChartResult { + repeated RaidSimResult raid_sim_result = 1; } message RaidSimRequestSplitRequest { diff --git a/sim/web/main.go b/sim/web/main.go index 6ce0aad539..28d9564073 100644 --- a/sim/web/main.go +++ b/sim/web/main.go @@ -25,7 +25,6 @@ import ( "github.com/wowsims/mop/sim/core" proto "github.com/wowsims/mop/sim/core/proto" "github.com/wowsims/mop/sim/core/simsignals" - googleProto "google.golang.org/protobuf/proto" ) @@ -273,6 +272,131 @@ func (s *server) setupAsyncServer() { w.Write(outbytes) }))) } +func (s *server) setupCharEndpoint() { + http.Handle("/getChartSims", corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + return + } + msg := &proto.RaidSimRequest{} + if err := googleProto.Unmarshal(body, msg); err != nil { + log.Printf("Failed to parse request: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + sims := prepareSimsToChart(msg, msg.GetChartInput()) + allResults := make(chan *proto.RaidSimResult, len(sims)) + + var wg sync.WaitGroup + for _, sim := range sims { + // a lot of the code here is repurposed from the code for running a single sim + wg.Add(1) + go func(sim *proto.RaidSimRequest, waitGroup *sync.WaitGroup) { + // reporter channel is handed into the core simulation. + // as the simulation advances it will push changes to the channel + // these changes will be consumed by the goroutine below so the asyncProgress endpoint can fetch the results. + reporter := make(chan *proto.ProgressMetrics, 100) + + // Generate a new async simulation + simProgress := s.addNewSim() + core.RunRaidSimConcurrentAsync(sim, reporter, sim.RequestId) + // Now launch a background process that pulls progress reports off the reporter channel + // and pushes it into the async progress cache. + go func() { + defer waitGroup.Done() + for { + select { + case <-time.After(time.Minute * 10): + // if we get no progress after 10 minutes, delete the pending sim and exit. + s.progMut.Lock() + delete(s.asyncProgresses, simProgress.id) + s.progMut.Unlock() + return + case progMetric := <-reporter: + if progMetric == nil { + return + } + simProgress.latestProgress.Store(progMetric) + if progMetric.FinalRaidResult != nil { + progMetric.FinalRaidResult.ChartInput = sim.ChartInput + progMetric.FinalRaidResult.Logs = "" + allResults <- progMetric.FinalRaidResult + log.Printf("finished for %s", sim.RequestId) + } + if progMetric.FinalRaidResult != nil || progMetric.FinalWeightResult != nil || progMetric.FinalBulkResult != nil { + return + } + } + } + }() + }(sim, &wg) + } + + // waitgroup is here because for some reason I couldn't make it work with + // a buffered channel, so I opted to just wait for all task to complete + // and manually close the channel + wg.Wait() + close(allResults) + + var toReturn []*proto.RaidSimResult + for finalRaidResult := range allResults { + toReturn = append(toReturn, finalRaidResult) + } + + outbytes, err := googleProto.Marshal(&proto.ChartResult{ + RaidSimResult: toReturn, + }) + if err != nil { + log.Printf("Failed to marshal result: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // why dig through JSON when I can log the data to stdout as csv, lol + // this could be deleted + log.Printf("first stat, first stat delta, second stat, second stat delta, dps, stdev") + for _, row := range toReturn { + log.Printf("%d,%d,%d,%d,%v,%v", + row.ChartInput.StatsToCompare[0], + row.ChartInput.CurrentValue, + row.ChartInput.StatsToCompare[1], + row.ChartInput.CurrentValueOther, + row.RaidMetrics.Dps.Avg, + row.RaidMetrics.Dps.Stdev, + ) + } + + // originally I was returning the data marshaled to JSON so I can also read them in the browser, + // irrelevant for my implementation, this can be anything protobuf as well. + w.Header().Set("Content-Type", "application/x-protobuf") + w.Write(outbytes) + }))) +} + +func prepareSimsToChart(simRequest *proto.RaidSimRequest, setup *proto.ChartInput) []*proto.RaidSimRequest { + lowerBound := setup.GetLowerBound() + upperBound := setup.GetUpperBound() + step := setup.GetStep() + statsToCompare := []int32{setup.StatsToCompare[0], setup.StatsToCompare[1]} + + simsToRun := (upperBound - lowerBound) / step + results := make([]*proto.RaidSimRequest, 0, simsToRun) + + for i := lowerBound; i <= upperBound; i = i + step { + copiedSim := googleProto.Clone(simRequest).(*proto.RaidSimRequest) + copiedSim.RequestId = fmt.Sprintf("%s-%d", simRequest.RequestId, i) + copiedSim.GetRaid().GetParties()[0].GetPlayers()[0].GetBonusStats().Stats[statsToCompare[0]] = copiedSim.GetRaid().GetParties()[0].GetPlayers()[0].GetBonusStats().Stats[statsToCompare[0]] + float64(i) + copiedSim.GetRaid().GetParties()[0].GetPlayers()[0].GetBonusStats().Stats[statsToCompare[1]] = copiedSim.GetRaid().GetParties()[0].GetPlayers()[0].GetBonusStats().Stats[statsToCompare[1]] - float64(i) + copiedSim.ChartInput.CurrentValue = i + copiedSim.ChartInput.CurrentValueOther = 0 + + results = append(results, copiedSim) + } + + return results +} + func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") @@ -287,6 +411,7 @@ func corsMiddleware(next http.Handler) http.Handler { } func (s *server) runServer(useFS bool, host string, launchBrowser bool, simName string, wasm bool, inputReader *bufio.Reader) { s.setupAsyncServer() + s.setupCharEndpoint() var fs http.Handler if useFS { diff --git a/ui/core/sim.ts b/ui/core/sim.ts index 0f253e12e7..be26ef8b2a 100644 --- a/ui/core/sim.ts +++ b/ui/core/sim.ts @@ -415,6 +415,15 @@ export class Sim { const request = this.makeRaidSimRequest(false); + // this I added because I couldn't be assed to create an entirely + // new piece of UI to handle a new protobuf message type + request.chartInput = { + statsToCompare: [Stat.StatCritRating , Stat.StatHasteRating], + step: 160, + lowerBound: -1600, + upperBound: 1600, + } + let result; // Only use worker base concurrency when running wasm. Local sim has native threading. if (await this.shouldUseWasmConcurrency()) { diff --git a/vite.build-workers.ts b/vite.build-workers.ts index 9784cd49ce..b3935f17f9 100644 --- a/vite.build-workers.ts +++ b/vite.build-workers.ts @@ -22,7 +22,7 @@ const args = minimist(process.argv.slice(2), { boolean: ['watch'] }); const buildWorkers = async () => { const { stdout } = await execAsync('go env GOROOT'); const GO_ROOT = stdout.replace('\n', ''); - const wasmExecutablePath = path.join(GO_ROOT, '/misc/wasm/wasm_exec.js'); + const wasmExecutablePath = path.join(GO_ROOT, '/lib/wasm/wasm_exec.js'); const wasmFile = await fs.readFile(wasmExecutablePath, 'utf8'); Object.entries(workers).forEach(async ([name, sourcePath]) => {