Precalculate index stats

This commit is contained in:
Pijus Kamandulis 2025-06-23 17:52:20 +03:00
parent d801debaf6
commit be637f4540
13 changed files with 243 additions and 81 deletions

13
constants/constants.go Normal file
View File

@ -0,0 +1,13 @@
package constants
import "time"
// time.Duration constants
const (
// RecalculateTimeWindowHighSharesJob interval
RecalculateTimeWindowHighSharesJobInterval = 1 * time.Minute
// RecalculateTopSharesJob interval
RecalculateTopSharesJobInterval = 30 * time.Second
// IngestorWatchInterval interval
IngestorWatchInterval = 30 * time.Second
)

View File

@ -17,6 +17,8 @@ const (
TopSharesCollectionName = "TopShares"
TopSharesAmount = 20
TimeWindowHighShareCollectionName = "TimeWindowHighShareStat"
)
func InitDatabase(path string) (*clover.DB, error) {
@ -65,6 +67,21 @@ func InitDatabase(path string) (*clover.DB, error) {
}
}
// Init TimeWindowHighShareStat collection
hasTimeWindowCollection, err := db.HasCollection(TimeWindowHighShareCollectionName)
if err != nil {
return nil, fmt.Errorf("failed to check TimeWindowHighShare collection: %v", err)
}
if !hasTimeWindowCollection {
if err := db.CreateCollection(TimeWindowHighShareCollectionName); err != nil {
return nil, fmt.Errorf("failed to create TimeWindowHighShare collection: %v", err)
}
if err := db.CreateIndex(TimeWindowHighShareCollectionName, "TimeWindowID"); err != nil {
return nil, fmt.Errorf("failed to create index for TimeWindowHighShare: %v", err)
}
}
return db, nil
}
@ -165,3 +182,37 @@ func ReplaceTopShares(db *clover.DB, shares []models.ShareLog) {
log.Printf("Replaced TopShares with %d shares", len(shares))
}
func GetTimeWindowHighShares(db *clover.DB) []models.TimeWindowHighShare {
results, err := db.FindAll(
c.NewQuery(TimeWindowHighShareCollectionName).
Sort(c.SortOption{Field: "TimeWindowID", Direction: 1}),
)
if err != nil {
log.Printf("failed to list time window high shares: %v", err)
return nil
}
timeWindowHighShares := make([]models.TimeWindowHighShare, len(results))
for idx, doc := range results {
var timeWindowHighShare models.TimeWindowHighShare
doc.Unmarshal(&timeWindowHighShare)
timeWindowHighShares[idx] = timeWindowHighShare
}
return timeWindowHighShares
}
func SetTimeWindowHighShare(db *clover.DB, share models.TimeWindowHighShare) error {
doc := document.NewDocumentOf(&share)
existingDoc, _ := db.FindFirst(c.NewQuery(TimeWindowHighShareCollectionName).
Where(c.Field("TimeWindowID").Eq(share.TimeWindowID)))
if existingDoc != nil {
db.ReplaceById(TimeWindowHighShareCollectionName, existingDoc.ObjectId(), doc)
} else {
db.InsertOne(TimeWindowHighShareCollectionName, doc)
}
return nil
}

View File

@ -35,5 +35,5 @@ func FormatCreateDate(createdate string) string {
t := time.Unix(sec, nsec)
return t.Format(time.DateTime)
}
return ""
return "-"
}

View File

@ -12,6 +12,7 @@ import (
"github.com/ostafen/clover/v2"
"github.com/ostafen/clover/v2/document"
"pool-stats/constants"
"pool-stats/database"
"pool-stats/models"
)
@ -26,7 +27,7 @@ func NewIngestor(db *clover.DB, path string) *Ingestor {
}
func (this *Ingestor) WatchAndIngest() {
ticker := time.NewTicker(30 * time.Second)
ticker := time.NewTicker(constants.IngestorWatchInterval)
defer ticker.Stop()
for {

View File

@ -0,0 +1,106 @@
package jobs
import (
"pool-stats/constants"
"pool-stats/database"
"pool-stats/models"
"pool-stats/notlinq"
"sort"
"time"
"github.com/ostafen/clover/v2"
)
type RecalculateTimeWindowHighSharesJob struct {
DB *clover.DB
}
func NewRecalculateTimeWindowHighSharesJob(db *clover.DB) *RecalculateTimeWindowHighSharesJob {
return &RecalculateTimeWindowHighSharesJob{DB: db}
}
func (job *RecalculateTimeWindowHighSharesJob) Run() error {
ticker := time.NewTicker(constants.RecalculateTimeWindowHighSharesJobInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
job.recalculateTimeWindowHighShares()
}
}
}
func (job *RecalculateTimeWindowHighSharesJob) recalculateTimeWindowHighShares() {
topShares := database.ListTopShares(job.DB)
sort.Slice(topShares, func(i, j int) bool {
return topShares[i].SDiff > topShares[j].SDiff
})
// All time high share
if len(topShares) > 0 {
allTimeHighShare := topShares[0]
allTimeHighShareStat := &models.TimeWindowHighShare{
TimeWindowID: "0-all-time",
TimeWindowName: "All Time",
SDiff: allTimeHighShare.SDiff,
Time: allTimeHighShare.CreateDate,
}
database.SetTimeWindowHighShare(job.DB, *allTimeHighShareStat)
}
// Other ranges
timeWindows := []struct {
ID string
Name string
Since time.Time
}{
{"1-hour", "Past Hour", time.Now().Add(-1 * time.Hour)},
{"2-day", "Past 24h", time.Now().Add(-24 * time.Hour)},
{"3-week", "Past 7d", time.Now().Add(-7 * 24 * time.Hour)},
}
for _, tw := range timeWindows {
// Can use one of top shares if in range,
// otherwise get highest share in range
var highestShare models.ShareLog
topSharesInRange := notlinq.
Where(topShares, func(s models.ShareLog) bool {
shareTime, err := s.ParseCreateDate()
if err != nil {
return false
}
return shareTime.After(tw.Since)
})
sort.Slice(topSharesInRange, func(i, j int) bool {
return topSharesInRange[i].SDiff > topSharesInRange[j].SDiff
})
if len(topSharesInRange) > 0 {
highestShare = topSharesInRange[0]
} else {
highestShareDocs, _ := database.GetHighestSharesInRange(
job.DB, database.CollectionName, tw.Since, 1)
if len(highestShareDocs) > 0 {
highestShare = highestShareDocs[0]
}
}
var timeWindowStat models.TimeWindowHighShare
if highestShare.SDiff > 0 {
timeWindowStat = models.TimeWindowHighShare{
TimeWindowID: tw.ID,
TimeWindowName: tw.Name,
SDiff: highestShare.SDiff,
Time: highestShare.CreateDate,
}
} else {
timeWindowStat = models.TimeWindowHighShare{
TimeWindowID: tw.ID,
TimeWindowName: tw.Name,
SDiff: 0,
Time: "-",
}
}
database.SetTimeWindowHighShare(job.DB, timeWindowStat)
}
}

View File

@ -1,6 +1,7 @@
package jobs
import (
"pool-stats/constants"
"pool-stats/database"
"pool-stats/helpers"
"pool-stats/models"
@ -20,7 +21,7 @@ func NewRecalculateTopSharesJob(db *clover.DB) *RecalculateTopSharesJob {
}
func (job *RecalculateTopSharesJob) Run() error {
ticker := time.NewTicker(10 * time.Second)
ticker := time.NewTicker(constants.RecalculateTopSharesJobInterval)
defer ticker.Stop()
for {

View File

@ -29,6 +29,9 @@ func main() {
topSharesRecalcJob := jobs.NewRecalculateTopSharesJob(db)
go topSharesRecalcJob.Run()
timeWindowHighSharesRecalcJob := jobs.NewRecalculateTimeWindowHighSharesJob(db)
go timeWindowHighSharesRecalcJob.Run()
webServer := web.NewWebServer(db, config.Port)
if err := webServer.Start(); err != nil {
log.Fatalf("Failed to start web server: %v", err)

View File

@ -29,10 +29,11 @@ type ShareLog struct {
Agent string `json:"agent"` // Miner agent string (e.g., bitaxe/BM1370)
}
type ShareStat struct {
Label string
Diff string
Time string
type TimeWindowHighShare struct {
TimeWindowID string `json:"time_window_id"` // Unique ID for the time window
TimeWindowName string `json:"time_window_name"` // Name of the time window (e.g., "Past Hour")
SDiff float64 `json:"share_diff"` // Difficulty of the highest share
Time string `json:"share_time"` // Time of the highest share
}
// ParseCreateDate can be used to convert ShareLog.CreateDate to time.Time

View File

@ -12,3 +12,13 @@ func UniqueBy[T any, K comparable](items []T, keySelector func(T) K) []T {
}
return result
}
func Where[T any](items []T, predicate func(T) bool) []T {
result := make([]T, 0)
for _, item := range items {
if predicate(item) {
result = append(result, item)
}
}
return result
}

View File

@ -1,50 +0,0 @@
package stats
import (
"time"
"github.com/ostafen/clover/v2"
"pool-stats/database"
"pool-stats/helpers"
"pool-stats/models"
)
func GetStats(db *clover.DB) ([]models.ShareStat, error) {
now := time.Now()
ranges := []struct {
Label string
Since time.Time
}{
{"Past Hour", now.Add(-1 * time.Hour)},
{"Past 24h", now.Add(-24 * time.Hour)},
{"Past 7d", now.Add(-7 * 24 * time.Hour)},
}
stats := []models.ShareStat{}
// All-time highest
doc, _ := database.GetHighestSharesInRange(db, database.CollectionName, time.Unix(0, 0), 1)
if doc != nil {
stats = append(stats, models.ShareStat{
Label: "All Time",
Diff: helpers.HumanDiff(doc[0].SDiff),
Time: helpers.ParseCreateDate(doc[0].CreateDate).Format(time.RFC822),
})
}
for _, r := range ranges {
doc, _ := database.GetHighestSharesInRange(db, database.CollectionName, r.Since, 1)
if doc != nil {
stats = append(stats, models.ShareStat{
Label: r.Label,
Diff: helpers.HumanDiff(doc[0].SDiff),
Time: helpers.ParseCreateDate(doc[0].CreateDate).Format(time.RFC822),
})
} else {
stats = append(stats, models.ShareStat{Label: r.Label, Diff: "-", Time: "-"})
}
}
return stats, nil
}

View File

@ -1,3 +1,4 @@
{{define "index"}}
<!DOCTYPE html>
<html>
<head>
@ -45,11 +46,13 @@
<th>Highest Share Diff</th>
<th>Time</th>
</tr>
{{range .}}
{{ range .Stats }}
<tr>
<td>{{.Label}}</td>
<td>{{.Diff}}</td>
<td>{{.Time}}</td>
<td>{{ .TimeWindowName }}</td>
<td>
{{ if ne .SDiff 0.0 }} {{ humanDiff .SDiff }} {{ else }} - {{ end }}
</td>
<td>{{ formatCreateDate .Time }}</td>
</tr>
{{ end }}
</table>
@ -61,3 +64,4 @@
</ul>
</body>
</html>
{{ end }}

View File

@ -1,19 +0,0 @@
package web
import (
"html/template"
"net/http"
"pool-stats/stats"
)
func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) {
shareStats, err := stats.GetStats(ws.db)
if err != nil {
http.Error(w, "Failed to load stats", 500)
return
}
tmpl := template.Must(template.ParseFiles("templates/index.html"))
tmpl.Execute(w, shareStats)
}

41
web/indexHandler.go Normal file
View File

@ -0,0 +1,41 @@
package web
import (
"html/template"
"net/http"
"pool-stats/database"
"pool-stats/helpers"
"pool-stats/models"
)
type IndexPageData struct {
Stats []models.TimeWindowHighShare
}
func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) {
tmpl := template.New("index").Funcs(template.FuncMap{
"humanDiff": helpers.HumanDiff,
"formatCreateDate": helpers.FormatCreateDate,
})
tmpl, err := tmpl.ParseFiles("templates/index.html")
if err != nil {
http.Error(w, "Failed to load template", 500)
return
}
tws := database.GetTimeWindowHighShares(ws.db)
if tws == nil {
http.Error(w, "Failed to load time window high shares", 500)
return
}
indexData := IndexPageData{
Stats: tws,
}
if err := tmpl.Execute(w, indexData); err != nil {
http.Error(w, "Failed to render template", 500)
println("Error rendering template:", err.Error())
return
}
}