Skip to content

Commit

Permalink
Implemented more primitive rate limiting to mirror the server code.
Browse files Browse the repository at this point in the history
  • Loading branch information
xStrom committed Apr 17, 2017
1 parent 7c493c4 commit 790b008
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 96 deletions.
66 changes: 66 additions & 0 deletions art/dota/dota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2017 Kaur Kuut
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dota

import (
"io/ioutil"

"github.com/xStrom/patriot/art"
)

const w, h = 50, 50
const x0, y0 = 405, 280
const x1, y1 = x0 + w - 1, y0 + h - 1

var resource = &art.Image{}

func init() {
data, err := ioutil.ReadFile("data/dota.png")
if err != nil {
panic("Failed to read dota.png")
}
err = resource.ParseKeyframe(1, data, true)
if err != nil {
panic("Failed to parse dota.png")
}
}

// Fixes any broken pixels in the provided image
func GetWork(image *art.Image, ignorePixels map[int]bool) *art.Pixel {
for x := x0; x <= x1; x++ {
for y := y0; y <= y1; y++ {
if ignorePixels[x|(y<<16)] {
continue
}
c1 := resource.At(x-x0, y-y0)
if c1 == art.Transparent {
continue
}
c2 := image.At(x, y)
if c1 != c2 {
return &art.Pixel{x, y, c1}
}
}
}
return nil
}

// TODO: Bounds check function, so that not every art needs to be looped through after every pixel update --- make a dirty region system
func CheckPixel(x, y, c int) {
// Make sure the pixel is even in bounds
if x >= x0 && x <= x1 && y >= y0 && y <= y1 {

}
}
Binary file added data/dota.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
160 changes: 70 additions & 90 deletions painter/painter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
package painter

import (
"net/http"
"sync"
"time"

"github.com/xStrom/patriot/art"
"github.com/xStrom/patriot/art/dota"
"github.com/xStrom/patriot/art/estcows"
"github.com/xStrom/patriot/art/estville"
"github.com/xStrom/patriot/log"
Expand All @@ -45,10 +47,22 @@ func Work(wg *sync.WaitGroup, image *art.Image) {
}
shutdown.ShutdownLock.RUnlock()

// Sleep until we can perform the next move
sleepUntilNextMove()

inFlightLock.Lock()

var p *art.Pixel

// #0 priority Dota 2 logo [Near mario]
if p == nil {
p = dota.GetWork(image, inFlight)
}

// #1 priority Estville [Bottom right project]
p := estville.GetWork(image, inFlight)
if p == nil {
p = estville.GetWork(image, inFlight)
}

// #2 priority Estonian flag [Classic above the fold flag]
/*
Expand All @@ -64,125 +78,91 @@ func Work(wg *sync.WaitGroup, image *art.Image) {

if p != nil {
inFlight[p.X|(p.Y<<16)] = true
dc := allocateDrawCall(image.At(p.X, p.Y))
go func(p *art.Pixel, dc *drawCall) {
if err := sp.DrawPixel(p.X, p.Y, p.C); err != nil {
dc.Cancel()
cost := drawCallCost(image.At(p.X, p.Y))
cs := addCycleCost(cost)
go func(p *art.Pixel, cs int64, cost int) {
//log.Infof("Requesting draw of %v:%v - %v", p.X, p.Y, p.C)
if err, statusCode := sp.DrawPixel(p.X, p.Y, p.C); err != nil {
// Don't remove the cycle cost in case of 403, because that means we hit the server rate limiting
if statusCode != http.StatusForbidden {
removeCycleCost(cs, cost)
}
log.Infof("Failed drawing %v:%v to %v, because: %v", p.X, p.Y, p.C, err)
} else {
dc.Finalize()
}
time.Sleep(5 * time.Second) // Allow another additional 5 seconds for realtime to update after the request is done
inFlightLock.Lock()
delete(inFlight, p.X|(p.Y<<16))
inFlightLock.Unlock()
}(p, dc)
}(p, cs, cost)
}

inFlightLock.Unlock()

// Sleep until we can perform the next move
if sleep := getTimeUntilNextMove(); sleep > 0 {
log.Infof("Sleeping %v", sleep)
time.Sleep(sleep)
}
}
}

type drawCall struct {
time time.Time
cost int
}

func (dc *drawCall) Cancel() {
drawcallsLock.Lock()
defer drawcallsLock.Unlock()
for i := range drawcalls {
if drawcalls[i] == dc {
drawcalls[i], drawcalls = drawcalls[len(drawcalls)-1], drawcalls[:len(drawcalls)-1]
return
// Prevent hot spin if there's nothing to do
if p == nil {
time.Sleep(100 * time.Millisecond)
}
}
}

func (dc *drawCall) Finalize() {
drawcallsLock.Lock()
dc.time = time.Now()
drawcallsLock.Unlock()
}

var drawcalls []*drawCall
var drawcallsLock sync.Mutex

const (
scorePerWindow = 50
scoreWindow = 10 * time.Second
scoreWindowSecs = 10
paintOverWhiteCost = 2
paintOverOtherCost = 5
)

func drawCallCost(oldColor int) int {
if oldColor == art.White {
return paintOverWhiteCost
}
/*
// DISABLED: White surface reduced cost seems broken on the server side
if oldColor == art.White {
return paintOverWhiteCost
}
*/
return paintOverOtherCost
}

func allocateDrawCall(oldColor int) *drawCall {
dc := &drawCall{cost: drawCallCost(oldColor)}
drawcallsLock.Lock()
drawcalls = append(drawcalls, dc)
drawcallsLock.Unlock()
return dc
}
var cycleCost int
var cycleStart int64
var cycleLock sync.Mutex

func getTimeUntilNextMove() time.Duration {
drawcallsLock.Lock()
func addCycleCost(cost int) int64 {
cycleLock.Lock()
defer cycleLock.Unlock()
cycleCost += cost
return cycleStart
}

// Clean up entries older than the configured time window
now := time.Now()
for i := 0; i < len(drawcalls); i++ {
if !drawcalls[i].time.IsZero() && now.Sub(drawcalls[i].time) >= scoreWindow {
drawcalls[i], drawcalls = drawcalls[len(drawcalls)-1], drawcalls[:len(drawcalls)-1]
i--
}
func removeCycleCost(start int64, cost int) {
cycleLock.Lock()
defer cycleLock.Unlock()
if cycleStart == start {
cycleCost -= cost
}
}

// Add up the score
score := 0
for i := range drawcalls {
score += drawcalls[i].cost
}
func sleepUntilNextMove() {
for {
cycleLock.Lock()

now := time.Now().Unix()
if cycleStart+scoreWindowSecs <= now {
cycleStart = now
cycleCost = 0
//log.Infof("New cycle started at %v", cycleStart)
cycleLock.Unlock()
return
}

drawcallsLock.Unlock()
// Can we do the next move?
if scorePerWindow-cycleCost >= paintOverOtherCost {
//log.Infof("Can still do another move (%v/%v)", cycleCost, scorePerWindow)
cycleLock.Unlock()
return
}

// Can we do the next move immediately?
if scorePerWindow-score >= paintOverOtherCost {
return 0
}
cycleLock.Unlock()

// Determine time until enough free moves for any color surface
for {
var until time.Duration
freeMoves := scorePerWindow - score
drawcallsLock.Lock()
now := time.Now()
for i := range drawcalls {
if !drawcalls[i].time.IsZero() {
freeMoves += drawcalls[i].cost
remaining := drawcalls[i].time.Sub(now)
if remaining > until {
until = remaining
}
if freeMoves >= paintOverOtherCost {
drawcallsLock.Unlock()
return until
} else {
log.Infof("Not enough free moves: %v", freeMoves)
}
}
}
drawcallsLock.Unlock()
time.Sleep(100 * time.Millisecond)
time.Sleep(1 * time.Second)
}
}
12 changes: 6 additions & 6 deletions sp/sp.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,25 @@ func FetchImage() ([]byte, int, error) {
}
}

func DrawPixel(x, y, c int) error {
func DrawPixel(x, y, c int) (error, int) {
t := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("https://josephg.com/sp/edit?x=%v&y=%v&c=%v", x, y, c), nil)
if err != nil {
return errors.Wrap(err, "Failed creating request")
return errors.Wrap(err, "Failed creating request"), -1
}
req.Header.Set("User-Agent", UserAgent)
resp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "Failed performing request")
return errors.Wrap(err, "Failed performing request"), -1
}
if resp.StatusCode != http.StatusOK {
return errors.Errorf("Got non-OK status: %v", resp.StatusCode)
return errors.Errorf("Got non-OK status: %v", resp.StatusCode), resp.StatusCode
}
if b, err := ioutil.ReadAll(resp.Body); err != nil {
return errors.Wrap(err, "Failed reading response")
return errors.Wrap(err, "Failed reading response"), resp.StatusCode
} else if len(b) > 0 {
log.Infof("Got response:\n%v", string(b))
}
log.Infof("Drew: %v - %v - %v [%dms]", x, y, c, time.Since(t)/time.Millisecond)
return nil
return nil, resp.StatusCode
}

0 comments on commit 790b008

Please sign in to comment.