guv
Version:
Grid Utilization Virgilante
144 lines (120 loc) • 4.4 kB
text/coffeescript
{ Governor } = require './governor'
config = require './config'
# note: names lowercased
costsHourly =
'standard-1x': 0.035 # 25 usd/mnt
'standard-2x': 0.070 # 50
'performance-m': 0.350 # 250
'performance-l': 0.700 # 500
'1x': 0.050 # legacy
'2x': 0.100 # legacy
'px': 0.800 # legacy
calculateCosts = (computeSeconds, dynoType) ->
costs = {}
for role, seconds of computeSeconds
type = dynoType[role]
type = 'standard-1x' if not type
type = type.toLowerCase()
hours = (seconds/(60*60))
cost = costsHourly[type]*hours
costs[role] = cost
return costs
# Accumulate the compute time used
class ComputeTimer
constructor: () ->
@accumulated = {} # role -> seconds
@previousTimes = {} # role -> timestamp
addState: (state, times) ->
for role, s of state
newTime = times[role]
#console.log 'ss', role, s.current_workers
@accumulated[role] = 0 if not @accumulated[role]?
if @previousTimes[role]
timeDiff = Math.ceil((newTime - @previousTimes[role])/(1000))
increment = (timeDiff * s.current_workers)
@accumulated[role] += increment
if s.new_workers and s.previous_workers
# compensate for boot-up/shutdown time?
bootTime = 60
#console.log 'adding', s.new_workers - s.previous_workers
change = Math.abs(s.new_workers - s.previous_workers)
@accumulated[role] += bootTime * change
@previousTimes[role] = newTime
arrayEquals = (a, b) ->
A = a.toString()
B = b.toString()
return A == B # Lazy
queueDataFromEvents = (cfg, events) ->
# sort events time-wise
events = events.sort (a, b) -> (a.timestamp - b.timestamp)
# calculate full set of queue data, as they would be returned by RabbitMQ
allRoles = Object.keys(cfg).filter((r) -> r != '*').sort()
data = []
lastTimestamp = 0
lastByRole = {}
for e in events
if e.timestamp < lastTimestamp
console.log 'WARN: unordered event data', e.timestamp, lastTimestamp
lastByRole[e.role] = e
lastTimestamp = e.timestamp
haveRoles = Object.keys(lastByRole).sort()
#console.log haveRoles, allRoles
if arrayEquals haveRoles, allRoles
queues = {}
timestamps = {}
for role, v of lastByRole
queue = cfg[role].queue
queues[queue] = v.jobs
timestamps[role] = v.timestamp
#console.log 'full', queues, lastByRole
data.push
queues: queues
timestamps: timestamps
lastByRole = {}
return data
# with a given config
# replay a set of GuvScaled events
# invariant: events are sorted according to increasing time
# determine an initial stable state, where we have internal state for all roles
# from this time on, calculate number of worker-compute-seconds (or minutes) we have
# based on this data, allow calculating cost (per role, per app, total)
#
# TODO: allow filtering on app?
# TODO: store some config identifier into events? hash of normalized config? so we can detect changes
# TODO: allow calculating min and max, costs from a config
# TODO: allow calculating typical costs, given total number of events for period
main = () ->
# node.js only
fs = require 'fs'
[ interpreter, prog, configFile, eventFile ] = process.argv
c = fs.readFileSync configFile
cfg = config.parse c
governor = new Governor cfg
compute = new ComputeTimer
#console.log governor.config
events = JSON.parse(fs.readFileSync(eventFile))
console.log "got #{events.length} events\n"
# XXX: have to combine all events at a given timestamp, to give queue data for everything at once
# if history was done per role instead of globally, this would not be neccesary
queueData = queueDataFromEvents cfg, events
for d in queueData
try
s = governor.nextState null, d.queues
compute.addState s, d.timestamps # TODO: keep timestamp state internally?
catch e
console.log d.queues, s?, e
console.log governor.history
console.log e.stack
types = {}
for role, val of cfg
types[role] = val.dynosize
# console.log 'dyno sizes', types
# console.log 'compute seconds', compute.accumulated
costs = calculateCosts compute.accumulated, types
# console.log 'costs', costs
total = 0
for role, v of costs
console.log "#{role}: #{v.toFixed()} USD"
total += v
console.log "Total: #{total.toFixed()} USD"
main() if not module.parent