Precalculate index stats
This commit is contained in:
parent
d801debaf6
commit
be637f4540
13
constants/constants.go
Normal file
13
constants/constants.go
Normal 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
|
||||
)
|
@ -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
|
||||
}
|
||||
|
@ -35,5 +35,5 @@ func FormatCreateDate(createdate string) string {
|
||||
t := time.Unix(sec, nsec)
|
||||
return t.Format(time.DateTime)
|
||||
}
|
||||
return ""
|
||||
return "-"
|
||||
}
|
||||
|
@ -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 {
|
||||
|
106
jobs/recalculateTimeWindowHighShares.go
Normal file
106
jobs/recalculateTimeWindowHighShares.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
3
main.go
3
main.go
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
{{define "index"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@ -45,13 +46,15 @@
|
||||
<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}}
|
||||
{{ end }}
|
||||
</table>
|
||||
|
||||
<ul>
|
||||
@ -61,3 +64,4 @@
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
|
@ -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
41
web/indexHandler.go
Normal 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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user