Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
127 changes: 126 additions & 1 deletion sim/web/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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", "*")
Expand All @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions ui/core/sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,15 @@

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 = {

Check failure on line 420 in ui/core/sim.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Type '{ statsToCompare: Stat[]; step: number; lowerBound: number; upperBound: number; }' is missing the following properties from type 'ChartInput': currentValue, currentValueOther
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()) {
Expand Down
2 changes: 1 addition & 1 deletion vite.build-workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, this path was changed at some point in the attributable, had to patch this or it wouldn't work locally, I guess it's irrelevant

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would actually be good to update, since this has been a proper path to wasm-exec.js for a while now.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just implemented a change that was merged into master supporting the go 1.24 change where they moved this. Don't forget to merge in the latest changes to your fork

const wasmFile = await fs.readFile(wasmExecutablePath, 'utf8');

Object.entries(workers).forEach(async ([name, sourcePath]) => {
Expand Down
Loading