Skip to content

Commit

Permalink
ticketpool chart data inline -> AJAX
Browse files Browse the repository at this point in the history
This removes inline data from the ticketpool page, using AJAX instead.

Initial chart data is also retrieved instead via AJAX, not just upon changing bar width.

The websocket call that retrieves the same data remains in the code, but is not used here.
  • Loading branch information
buck54321 authored and chappjc committed Dec 7, 2018
1 parent ce5e93c commit cb5d009
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 238 deletions.
1 change: 1 addition & 0 deletions api/apirouter.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func NewAPIRouter(app *appContext, useRealIP bool) apiMux {
mux.Route("/ticketpool", func(r chi.Router) {
r.Get("/", app.getTicketPoolByDate)
r.With(m.TicketPoolCtx).Get("/bydate/{tp}", app.getTicketPoolByDate)
r.Get("/charts", app.getTicketPoolCharts)
})

mux.NotFound(func(w http.ResponseWriter, r *http.Request) {
Expand Down
46 changes: 41 additions & 5 deletions api/apiroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type DataSourceLite interface {
GetAddressTransactionsRawWithSkip(addr string, count, skip int) []*apitypes.AddressTxRaw
SendRawTransaction(txhex string) (string, error)
GetExplorerAddress(address string, count, offset int64) (*explorer.AddressInfo, txhelpers.AddressType, txhelpers.AddressError)
GetMempoolPriceCountTime() *apitypes.PriceCountTime
}

// DataSourceAux specifies an interface for advanced data collection using the
Expand All @@ -97,7 +98,7 @@ type DataSourceAux interface {
TxHistoryData(address string, addrChart dbtypes.HistoryChart,
chartGroupings dbtypes.TimeBasedGrouping) (*dbtypes.ChartsData, error)
TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) (
[]*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error)
*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error)
AgendaVotes(agendaID string, chartType int) (*dbtypes.AgendaVoteChoices, error)
}

Expand Down Expand Up @@ -841,6 +842,41 @@ func (c *appContext) getSSTxDetails(w http.ResponseWriter, r *http.Request) {
writeJSON(w, sstxDetails, c.getIndentQuery(r))
}

// getTicketPoolCharts pulls the initial data to populate the /ticketpool page
// charts.
func (c *appContext) getTicketPoolCharts(w http.ResponseWriter, r *http.Request) {
if c.LiteMode {
// not available in lite mode
http.Error(w, "not available in lite mode", 422)
return
}

timeChart, priceChart, donutChart, height, err := c.AuxDataSource.TicketPoolVisualization(dbtypes.AllGrouping)
if dbtypes.IsTimeoutErr(err) {
apiLog.Errorf("TicketPoolVisualization: %v", err)
http.Error(w, "Database timeout.", http.StatusServiceUnavailable)
return
}
if err != nil {
apiLog.Errorf("Unable to get ticket pool charts: %v", err)
http.Error(w, http.StatusText(http.StatusUnprocessableEntity), http.StatusUnprocessableEntity)
return
}

mp := c.BlockData.GetMempoolPriceCountTime()

response := &apitypes.TicketPoolChartsData{
ChartHeight: height,
TimeChart: timeChart,
PriceChart: priceChart,
DonutChart: donutChart,
Mempool: mp,
}

writeJSON(w, response, c.getIndentQuery(r))

}

func (c *appContext) getTicketPoolByDate(w http.ResponseWriter, r *http.Request) {
if c.LiteMode {
// not available in lite mode
Expand All @@ -858,7 +894,7 @@ func (c *appContext) getTicketPoolByDate(w http.ResponseWriter, r *http.Request)
// TicketPoolVisualization here even though it returns a lot of data not
// needed by this request.
interval := dbtypes.TimeGroupingFromStr(tp)
barCharts, _, height, err := c.AuxDataSource.TicketPoolVisualization(interval)
timeChart, _, _, height, err := c.AuxDataSource.TicketPoolVisualization(interval)
if dbtypes.IsTimeoutErr(err) {
apiLog.Errorf("TicketPoolVisualization: %v", err)
http.Error(w, "Database timeout.", http.StatusServiceUnavailable)
Expand All @@ -871,11 +907,11 @@ func (c *appContext) getTicketPoolByDate(w http.ResponseWriter, r *http.Request)
}

tpResponse := struct {
Height uint64 `json:"height"`
PoolByDate *dbtypes.PoolTicketsData `json:"ticket_pool_data"`
Height uint64 `json:"height"`
TimeChart *dbtypes.PoolTicketsData `json:"time_chart"`
}{
height,
barCharts[0], // purchase time distribution
timeChart, // purchase time distribution
}

writeJSON(w, tpResponse, c.getIndentQuery(r))
Expand Down
17 changes: 17 additions & 0 deletions api/types/apitypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,20 @@ type MempoolTicketDetails struct {
// TicketsDetails is an array of pointers of TicketDetails used in
// MempoolTicketDetails
type TicketsDetails []*TicketDetails

// TicketPoolChartsData is for data used to display ticket pool statistics at
// /ticketpool.
type TicketPoolChartsData struct {
ChartHeight uint64 `json:"height"`
TimeChart *dbtypes.PoolTicketsData `json:"time_chart"`
PriceChart *dbtypes.PoolTicketsData `json:"price_chart"`
DonutChart *dbtypes.PoolTicketsData `json:"donut_chart"`
Mempool *PriceCountTime `json:"mempool"`
}

// PriceCountTime is a basic set of information about ticket in the mempool.
type PriceCountTime struct {
Price float64 `json:"price"`
Count int `json:"count"`
Time dbtypes.TimeDef `json:"time"`
}
70 changes: 34 additions & 36 deletions db/dcrpg/pgblockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,32 +74,33 @@ func (d *DevFundBalance) Balance() *explorer.AddressBalance {
// fetching the same information.
type ticketPoolDataCache struct {
sync.RWMutex
Height map[dbtypes.TimeBasedGrouping]uint64
// BarGraphsCache persists data for the Ticket purchase distribution chart
// and Ticket Price Distribution chart
BarGraphsCache map[dbtypes.TimeBasedGrouping][]*dbtypes.PoolTicketsData
Height map[dbtypes.TimeBasedGrouping]uint64
TimeGraphCache map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData
PriceGraphCache map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData
// DonutGraphCache persist data for the Number of tickets outputs pie chart.
DonutGraphCache map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData
}

// ticketPoolGraphsCache persists the latest ticketpool data queried from the db.
var ticketPoolGraphsCache = &ticketPoolDataCache{
Height: make(map[dbtypes.TimeBasedGrouping]uint64),
BarGraphsCache: make(map[dbtypes.TimeBasedGrouping][]*dbtypes.PoolTicketsData),
TimeGraphCache: make(map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData),
PriceGraphCache: make(map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData),
DonutGraphCache: make(map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData),
}

// TicketPoolData is a thread-safe way to access the ticketpool graphs data
// stored in the cache.
func TicketPoolData(interval dbtypes.TimeBasedGrouping, height uint64) (barGraphs []*dbtypes.PoolTicketsData,
donutChart *dbtypes.PoolTicketsData, actualHeight uint64, intervalFound, isStale bool) {
func TicketPoolData(interval dbtypes.TimeBasedGrouping, height uint64) (timeGraph *dbtypes.PoolTicketsData,
priceGraph *dbtypes.PoolTicketsData, donutChart *dbtypes.PoolTicketsData, actualHeight uint64, intervalFound, isStale bool) {
ticketPoolGraphsCache.RLock()
defer ticketPoolGraphsCache.RUnlock()

var found bool
barGraphs, intervalFound = ticketPoolGraphsCache.BarGraphsCache[interval]
donutChart, found = ticketPoolGraphsCache.DonutGraphCache[interval]
intervalFound = intervalFound && found
var tFound, pFound, dFound bool
timeGraph, tFound = ticketPoolGraphsCache.TimeGraphCache[interval]
priceGraph, pFound = ticketPoolGraphsCache.PriceGraphCache[interval]
donutChart, dFound = ticketPoolGraphsCache.DonutGraphCache[interval]
intervalFound = tFound && pFound && dFound

actualHeight = ticketPoolGraphsCache.Height[interval]
isStale = ticketPoolGraphsCache.Height[interval] != height
Expand All @@ -110,13 +111,14 @@ func TicketPoolData(interval dbtypes.TimeBasedGrouping, height uint64) (barGraph
// UpdateTicketPoolData updates the ticket pool cache with the latest data fetched.
// This is a thread-safe way to update ticket pool cache data. TryLock helps avoid
// stacking calls to update the cache.
func UpdateTicketPoolData(interval dbtypes.TimeBasedGrouping, barGraphs []*dbtypes.PoolTicketsData,
donutcharts *dbtypes.PoolTicketsData, height uint64) {
func UpdateTicketPoolData(interval dbtypes.TimeBasedGrouping, timeGraph *dbtypes.PoolTicketsData,
priceGraph *dbtypes.PoolTicketsData, donutcharts *dbtypes.PoolTicketsData, height uint64) {
ticketPoolGraphsCache.Lock()
defer ticketPoolGraphsCache.Unlock()

ticketPoolGraphsCache.Height[interval] = height
ticketPoolGraphsCache.BarGraphsCache[interval] = barGraphs
ticketPoolGraphsCache.TimeGraphCache[interval] = timeGraph
ticketPoolGraphsCache.PriceGraphCache[interval] = priceGraph
ticketPoolGraphsCache.DonutGraphCache[interval] = donutcharts
}

Expand Down Expand Up @@ -987,14 +989,14 @@ func (pgb *ChainDB) TimeBasedIntervals(timeGrouping dbtypes.TimeBasedGrouping,
// a query and updates the cache. If there is no cached data for the interval,
// this will launch a new query for the data if one is not already running, and
// if one is running, it will wait for the query to complete.
func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) ([]*dbtypes.PoolTicketsData,
*dbtypes.PoolTicketsData, uint64, error) {
func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) (*dbtypes.PoolTicketsData,
*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) {
// Attempt to retrieve data for the current block from cache.
heightSeen := pgb.Height() // current block seen *by the ChainDB*
barcharts, donutCharts, height, intervalFound, stale := TicketPoolData(interval, heightSeen)
timeChart, priceChart, donutCharts, height, intervalFound, stale := TicketPoolData(interval, heightSeen)
if intervalFound && !stale {
// The cache was fresh.
return barcharts, donutCharts, height, nil
return timeChart, priceChart, donutCharts, height, nil
}

// Cache is stale or empty. Attempt to gain updater status.
Expand All @@ -1009,72 +1011,68 @@ func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping)
defer pgb.tpUpdatePermission[interval].Unlock()
// Try again to pull it from cache now that the update is completed.
heightSeen = pgb.Height()
barcharts, donutCharts, height, intervalFound, stale = TicketPoolData(interval, heightSeen)
timeChart, priceChart, donutCharts, height, intervalFound, stale = TicketPoolData(interval, heightSeen)
// We waited for the updater of this interval, so it should be found
// at this point. If not, this is an error.
if !intervalFound {
log.Errorf("Charts data for interval %v failed to update.", interval)
return nil, nil, 0, fmt.Errorf("no charts data available")
return nil, nil, nil, 0, fmt.Errorf("no charts data available")
}
if stale {
log.Warnf("Charts data for interval %v updated, but still stale.", interval)
}
}
// else return the stale data instead of waiting.

return barcharts, donutCharts, height, nil
return timeChart, priceChart, donutCharts, height, nil
}
// This goroutine is now the cache updater.
defer pgb.tpUpdatePermission[interval].Unlock()

// Retrieve chart data for best block in DB.
var err error
barcharts, donutCharts, height, err = pgb.ticketPoolVisualization(interval)
timeChart, priceChart, donutCharts, height, err = pgb.ticketPoolVisualization(interval)
if err != nil {
log.Errorf("Failed to fetch ticket pool data: %v", err)
return nil, nil, 0, err
return nil, nil, nil, 0, err
}

// Update the cache with the new ticket pool data.
UpdateTicketPoolData(interval, barcharts, donutCharts, height)
UpdateTicketPoolData(interval, timeChart, priceChart, donutCharts, height)

return barcharts, donutCharts, height, nil
return timeChart, priceChart, donutCharts, height, nil
}

// ticketPoolVisualization fetches the following ticketpool data: tickets
// grouped on the specified interval, tickets grouped by price, and ticket
// counts by ticket type (solo, pool, other split). The interval may be one of:
// "mo", "wk", "day", or "all". The data is needed to populate the ticketpool
// graphs. The data grouped by time and price are returned in a slice.
func (pgb *ChainDB) ticketPoolVisualization(interval dbtypes.TimeBasedGrouping) (byTimeAndPrice []*dbtypes.PoolTicketsData,
byInputs *dbtypes.PoolTicketsData, height uint64, err error) {
func (pgb *ChainDB) ticketPoolVisualization(interval dbtypes.TimeBasedGrouping) (timeChart *dbtypes.PoolTicketsData,
priceChart *dbtypes.PoolTicketsData, byInputs *dbtypes.PoolTicketsData, height uint64, err error) {
// Ensure DB height is the same before and after queries since they are not
// atomic. Initial height:
height = pgb.Height()

for {
// Latest block where mature tickets may have been mined.
maturityBlock := pgb.TicketPoolBlockMaturity()

// Tickets grouped by time interval
ticketsByTime, err := pgb.TicketPoolByDateAndInterval(maturityBlock, interval)
timeChart, err = pgb.TicketPoolByDateAndInterval(maturityBlock, interval)
if err != nil {
return nil, nil, 0, err
return nil, nil, nil, 0, err
}

// Tickets grouped by price
ticketsByPrice, err := pgb.TicketsByPrice(maturityBlock)
priceChart, err = pgb.TicketsByPrice(maturityBlock)
if err != nil {
return nil, nil, 0, err
return nil, nil, nil, 0, err
}

// Return time- and price-grouped data in a slice
byTimeAndPrice = []*dbtypes.PoolTicketsData{ticketsByTime, ticketsByPrice}

// Tickets grouped by number of inputs.
byInputs, err = pgb.TicketsByInputCount()
if err != nil {
return nil, nil, 0, err
return nil, nil, nil, 0, err
}

heightEnd := pgb.Height()
Expand Down
6 changes: 6 additions & 0 deletions db/dcrsqlite/apisource.go
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,12 @@ func (db *wiredDB) GetMempoolSSTxDetails(N int) *apitypes.MempoolTicketDetails {
return &mpTicketDetails
}

// GetMempoolPriceCountTime retreives from mempool: the ticket price, the number
// of tickets in mempool, the time of the first ticket.
func (db *wiredDB) GetMempoolPriceCountTime() *apitypes.PriceCountTime {
return db.MPC.GetTicketPriceCountTime(int(db.params.MaxFreshStakePerBlock))
}

// GetAddressTransactionsWithSkip returns an apitypes.Address Object with at most the
// last count transactions the address was in
func (db *wiredDB) GetAddressTransactionsWithSkip(addr string, count, skip int) *apitypes.Address {
Expand Down
2 changes: 1 addition & 1 deletion explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type explorerDataSource interface {
BlockStatus(hash string) (dbtypes.BlockStatus, error)
BlockFlags(hash string) (bool, bool, error)
AddressMetrics(addr string) (*dbtypes.AddressMetrics, error)
TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) ([]*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error)
TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) (*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error)
TransactionBlocks(hash string) ([]*dbtypes.BlockStatus, []uint32, error)
Transaction(txHash string) ([]*dbtypes.Tx, error)
VinsForTx(*dbtypes.Tx) (vins []dbtypes.VinTxProperty, prevPkScripts []string, scriptVersions []uint16, err error)
Expand Down
33 changes: 1 addition & 32 deletions explorer/explorerroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,44 +637,13 @@ func (exp *explorerUI) Ticketpool(w http.ResponseWriter, r *http.Request) {
"Ticketpool page cannot run in lite mode", "", ExpStatusNotSupported)
return
}
interval := dbtypes.AllGrouping

barGraphs, donutChart, height, err := exp.explorerSource.TicketPoolVisualization(interval)
if exp.timeoutErrorPage(w, err, "TicketPoolVisualization") {
return
}
if err != nil {
log.Errorf("Template execute failure: %v", err)
exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, "", ExpStatusError)
return
}

var mp dbtypes.PoolTicketsData
exp.MempoolData.RLock()
if len(exp.MempoolData.Tickets) > 0 {
t := time.Unix(exp.MempoolData.Tickets[0].Time, 0)
mp.Time = append(mp.Time, dbtypes.TimeDef{T: t})
mp.Price = append(mp.Price, exp.MempoolData.Tickets[0].TotalOut)
mp.Mempool = append(mp.Mempool, uint64(len(exp.MempoolData.Tickets)))
} else {
log.Debug("No tickets exist in the mempool")
}
exp.MempoolData.RUnlock()

str, err := exp.templates.execTemplateToString("ticketpool", struct {
*CommonPageData
NetName string
ChartsHeight uint64
ChartData []*dbtypes.PoolTicketsData
GroupedData *dbtypes.PoolTicketsData
Mempool *dbtypes.PoolTicketsData
NetName string
}{
CommonPageData: exp.commonData(),
NetName: exp.NetName,
ChartsHeight: height,
ChartData: barGraphs,
GroupedData: donutChart,
Mempool: &mp,
})

if err != nil {
Expand Down
Loading

0 comments on commit cb5d009

Please sign in to comment.