diff --git a/README.md b/README.md index 533205a..e98bab7 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ Production-oriented Go web application for ingesting CPU benchmark results, stor ## Features - `POST /api/submit` accepts either `application/json` or `multipart/form-data`. -- `GET /api/search` performs case-insensitive token matching against submitter/general fields and CPU brand strings, with explicit thread-mode, platform, intensity, and duration filters. +- `GET /api/search` performs case-insensitive token matching against submitter/general fields and CPU brand strings, with explicit thread-mode, platform, intensity, duration, and sort controls. - `GET /` renders the latest submissions with search and pagination. +- The dashboard follows the system light/dark preference by default and includes a manual theme toggle in the top-right corner. - BadgerDB stores each submission under a reverse-timestamp key so native iteration returns newest records first. - A startup-loaded in-memory search index prevents full DB deserialization for every query. - Graceful shutdown closes the HTTP server and BadgerDB cleanly to avoid lock issues. @@ -123,13 +124,14 @@ Query parameters: - `cpu`: token-matches `cpuInfo.brandString` - `thread`: `single` or `multi` - `platform`: `windows`, `linux`, or `macos` +- `sort`: `newest`, `oldest`, `score_desc`, `score_asc`, `mops_desc`, or `mops_asc` - `intensity`: exact match on `config.intensity` - `durationSecs`: exact match on `config.durationSecs` Example: ```bash -curl "http://localhost:8080/api/search?text=intel&cpu=13700&thread=multi&platform=windows&intensity=10&durationSecs=30" +curl "http://localhost:8080/api/search?text=intel&cpu=13700&thread=multi&platform=windows&sort=score_desc&intensity=10&durationSecs=30" ``` ### `GET /` @@ -141,6 +143,7 @@ Query parameters: - `cpu` - `thread` - `platform` +- `sort` - `intensity` - `durationSecs` @@ -149,7 +152,7 @@ Examples: ```text http://localhost:8080/ http://localhost:8080/?page=2 -http://localhost:8080/?text=anonymous&cpu=ryzen&thread=multi&platform=windows&intensity=10&durationSecs=20 +http://localhost:8080/?text=anonymous&cpu=ryzen&thread=multi&platform=windows&sort=score_desc&intensity=10&durationSecs=20 ``` ## Request Examples @@ -186,7 +189,7 @@ curl -X POST "http://localhost:8080/api/submit" \ - canonical submission payload - normalized general search text - normalized CPU brand text -- Searches scan the in-memory ordered slice rather than reopening and deserializing Badger values for every request, and apply explicit platform, thread-mode, intensity, and duration filters in memory. +- Searches scan the in-memory ordered slice rather than reopening and deserializing Badger values for every request, apply explicit platform, thread-mode, intensity, and duration filters in memory, then optionally sort the matching results by submission time, score, or MOps/sec. ## Docker diff --git a/http/search.http b/http/search.http index 465cc6f..175a272 100644 --- a/http/search.http +++ b/http/search.http @@ -1,9 +1,9 @@ -GET http://localhost:8080/api/search?text=intel&cpu=13700&thread=multi&platform=windows&intensity=10&durationSecs=30 +GET http://localhost:8080/api/search?text=intel&cpu=13700&thread=multi&platform=windows&sort=score_desc&intensity=10&durationSecs=30 ### -GET http://localhost:8080/api/search?text=anonymous&thread=single&platform=linux&intensity=1&durationSecs=10 +GET http://localhost:8080/api/search?text=anonymous&thread=single&platform=linux&sort=oldest&intensity=1&durationSecs=10 ### -GET http://localhost:8080/?page=1&text=lab&cpu=ryzen&thread=multi&platform=windows&intensity=10&durationSecs=20 +GET http://localhost:8080/?page=1&text=lab&cpu=ryzen&thread=multi&platform=windows&sort=mops_desc&intensity=10&durationSecs=20 diff --git a/lib/store/store.go b/lib/store/store.go index abe5745..85c63ea 100644 --- a/lib/store/store.go +++ b/lib/store/store.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "math" + "sort" "strconv" "strings" "sync" @@ -116,11 +117,12 @@ func (s *Store) ListSubmissions(page, pageSize int) ([]model.Submission, int) { return results, total } -func (s *Store) SearchSubmissions(text, cpu, thread, platform string, intensity, durationSecs int) []model.Submission { +func (s *Store) SearchSubmissions(text, cpu, thread, platform, sortBy string, intensity, durationSecs int) []model.Submission { queryText := normalizeSearchText(text) cpuText := normalizeSearchText(cpu) thread = normalizeThreadFilter(thread) platform = normalizePlatformFilter(platform) + sortBy = normalizeSortOption(sortBy) s.mu.RLock() defer s.mu.RUnlock() @@ -159,6 +161,7 @@ func (s *Store) SearchSubmissions(text, cpu, thread, platform string, intensity, results = append(results, *model.CloneSubmission(record.submission)) } + sortSubmissions(results, sortBy) return results } @@ -296,6 +299,70 @@ func normalizePlatformFilter(value string) string { } } +func normalizeSortOption(value string) string { + switch strings.ToLower(strings.TrimSpace(value)) { + case "", "newest": + return "newest" + case "oldest": + return "oldest" + case "score_desc": + return "score_desc" + case "score_asc": + return "score_asc" + case "mops_desc": + return "mops_desc" + case "mops_asc": + return "mops_asc" + default: + return "newest" + } +} + +func sortSubmissions(submissions []model.Submission, sortBy string) { + switch sortBy { + case "oldest": + sort.SliceStable(submissions, func(i, j int) bool { + if submissions[i].SubmittedAt.Equal(submissions[j].SubmittedAt) { + return submissions[i].SubmissionID < submissions[j].SubmissionID + } + + return submissions[i].SubmittedAt.Before(submissions[j].SubmittedAt) + }) + case "score_desc": + sort.SliceStable(submissions, func(i, j int) bool { + if submissions[i].Score == submissions[j].Score { + return submissions[i].SubmittedAt.After(submissions[j].SubmittedAt) + } + + return submissions[i].Score > submissions[j].Score + }) + case "score_asc": + sort.SliceStable(submissions, func(i, j int) bool { + if submissions[i].Score == submissions[j].Score { + return submissions[i].SubmittedAt.After(submissions[j].SubmittedAt) + } + + return submissions[i].Score < submissions[j].Score + }) + case "mops_desc": + sort.SliceStable(submissions, func(i, j int) bool { + if submissions[i].MOpsPerSec == submissions[j].MOpsPerSec { + return submissions[i].SubmittedAt.After(submissions[j].SubmittedAt) + } + + return submissions[i].MOpsPerSec > submissions[j].MOpsPerSec + }) + case "mops_asc": + sort.SliceStable(submissions, func(i, j int) bool { + if submissions[i].MOpsPerSec == submissions[j].MOpsPerSec { + return submissions[i].SubmittedAt.After(submissions[j].SubmittedAt) + } + + return submissions[i].MOpsPerSec < submissions[j].MOpsPerSec + }) + } +} + func pageBounds(page, pageSize, total int) (int, int, int) { if pageSize <= 0 { pageSize = 50 diff --git a/lib/web/app.go b/lib/web/app.go index f20f9e3..98049b9 100644 --- a/lib/web/app.go +++ b/lib/web/app.go @@ -33,6 +33,7 @@ type indexPageData struct { QueryCPU string QueryThread string QueryPlatform string + QuerySort string QueryIntensity int QueryDuration int Page int @@ -104,6 +105,7 @@ func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) { cpu := strings.TrimSpace(r.URL.Query().Get("cpu")) thread := strings.TrimSpace(r.URL.Query().Get("thread")) platform := strings.TrimSpace(r.URL.Query().Get("platform")) + sortBy := normalizeSortFilter(r.URL.Query().Get("sort")) intensity := parsePositiveInt(r.URL.Query().Get("intensity"), 0) durationSecs := parsePositiveInt(r.URL.Query().Get("durationSecs"), 0) @@ -112,8 +114,8 @@ func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) { totalCount int ) - if text != "" || cpu != "" || thread != "" || platform != "" || intensity > 0 || durationSecs > 0 { - matches := a.store.SearchSubmissions(text, cpu, thread, platform, intensity, durationSecs) + if text != "" || cpu != "" || thread != "" || platform != "" || intensity > 0 || durationSecs > 0 || sortBy != "newest" { + matches := a.store.SearchSubmissions(text, cpu, thread, platform, sortBy, intensity, durationSecs) totalCount = len(matches) start, end, normalizedPage := pageBounds(page, a.pageSize, totalCount) page = normalizedPage @@ -134,6 +136,7 @@ func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) { QueryCPU: cpu, QueryThread: normalizeThreadFilter(thread), QueryPlatform: normalizePlatformFilter(platform), + QuerySort: sortBy, QueryIntensity: intensity, QueryDuration: durationSecs, Page: page, @@ -141,8 +144,8 @@ func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) { TotalCount: totalCount, ShowingFrom: showingFrom, ShowingTo: showingTo, - PrevURL: buildIndexURL(max(1, page-1), text, cpu, thread, platform, intensity, durationSecs), - NextURL: buildIndexURL(max(1, min(totalPageCount, page+1)), text, cpu, thread, platform, intensity, durationSecs), + PrevURL: buildIndexURL(max(1, page-1), text, cpu, thread, platform, sortBy, intensity, durationSecs), + NextURL: buildIndexURL(max(1, min(totalPageCount, page+1)), text, cpu, thread, platform, sortBy, intensity, durationSecs), } if err := a.templates.ExecuteTemplate(w, "index.html", data); err != nil { @@ -163,6 +166,7 @@ func (a *App) handleSearch(w http.ResponseWriter, r *http.Request) { r.URL.Query().Get("cpu"), r.URL.Query().Get("thread"), r.URL.Query().Get("platform"), + r.URL.Query().Get("sort"), parsePositiveInt(r.URL.Query().Get("intensity"), 0), parsePositiveInt(r.URL.Query().Get("durationSecs"), 0), ) @@ -359,7 +363,7 @@ func visibleBounds(page, pageSize, visibleCount, total int) (int, int) { return from, to } -func buildIndexURL(page int, text, cpu, thread, platform string, intensity, durationSecs int) string { +func buildIndexURL(page int, text, cpu, thread, platform, sortBy string, intensity, durationSecs int) string { if page < 1 { page = 1 } @@ -378,6 +382,9 @@ func buildIndexURL(page int, text, cpu, thread, platform string, intensity, dura if normalizedPlatform := normalizePlatformFilter(platform); normalizedPlatform != "" { values.Set("platform", normalizedPlatform) } + if normalizedSort := normalizeSortFilter(sortBy); normalizedSort != "newest" { + values.Set("sort", normalizedSort) + } if intensity > 0 { values.Set("intensity", strconv.Itoa(intensity)) } @@ -430,6 +437,25 @@ func normalizePlatformFilter(value string) string { } } +func normalizeSortFilter(value string) string { + switch strings.ToLower(strings.TrimSpace(value)) { + case "", "newest": + return "newest" + case "oldest": + return "oldest" + case "score_desc": + return "score_desc" + case "score_asc": + return "score_asc" + case "mops_desc": + return "mops_desc" + case "mops_asc": + return "mops_asc" + default: + return "newest" + } +} + func formatInt64(value int64) string { negative := value < 0 if negative { diff --git a/templates/index.html b/templates/index.html index 96bd81a..01dfb22 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,11 +5,44 @@
CPU Benchmark Platform
+CPU Benchmark Platform
+ +