Skip to content

Commit ff602ae

Browse files
authored
Merge pull request #563 from markus-wa/s2-perf-improvements
Source 2 performance improvements
2 parents 67ecfa4 + e7c8b7f commit ff602ae

13 files changed

+78
-45
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/llgcode/draw2d v0.0.0-20230723155556-e595d7c7e75e
77
github.com/markus-wa/go-heatmap/v2 v2.0.0
88
github.com/markus-wa/go-unassert v0.1.3
9-
github.com/markus-wa/gobitread v0.2.3
9+
github.com/markus-wa/gobitread v0.2.4
1010
github.com/markus-wa/godispatch v1.4.1
1111
github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7
1212
github.com/markus-wa/quickhull-go/v2 v2.2.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ github.com/markus-wa/go-unassert v0.1.3 h1:4N2fPLUS3929Rmkv94jbWskjsLiyNT2yQpCul
2121
github.com/markus-wa/go-unassert v0.1.3/go.mod h1:/pqt7a0LRmdsRNYQ2nU3SGrXfw3bLXrvIkakY/6jpPY=
2222
github.com/markus-wa/gobitread v0.2.3 h1:COx7dtYQ7Q+77hgUmD+O4MvOcqG7y17RP3Z7BbjRvPs=
2323
github.com/markus-wa/gobitread v0.2.3/go.mod h1:PcWXMH4gx7o2CKslbkFkLyJB/aHW7JVRG3MRZe3PINg=
24+
github.com/markus-wa/gobitread v0.2.4 h1:BDr3dZnsqntDD4D8E7DzhkQlASIkQdfxCXLhWcI2K5A=
25+
github.com/markus-wa/gobitread v0.2.4/go.mod h1:PcWXMH4gx7o2CKslbkFkLyJB/aHW7JVRG3MRZe3PINg=
2426
github.com/markus-wa/godispatch v1.4.1 h1:Cdff5x33ShuX3sDmUbYWejk7tOuoHErFYMhUc2h7sLc=
2527
github.com/markus-wa/godispatch v1.4.1/go.mod h1:tk8L0yzLO4oAcFwM2sABMge0HRDJMdE8E7xm4gK/+xM=
2628
github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7 h1:aR9pvnlnBxifXBmzidpAiq2prLSGlkhE904qnk2sCz4=

pkg/demoinfocs/common/player.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type Player struct {
1616
demoInfoProvider demoInfoProvider // provider for demo info such as tick-rate or current tick
1717

1818
SteamID64 uint64 // 64-bit representation of the user's Steam ID. See https://developer.valvesoftware.com/wiki/SteamID
19-
LastAlivePosition r3.Vector // The location where the player was last alive. Should be equal to Position if the player is still alive.
19+
LastAlivePosition r3.Vector // Deprecated: will be removed in v5 due to performance concerns, track this yourself.
2020
UserID int // Mostly used in game-events to address this player
2121
Name string // Steam / in-game user name
2222
Inventory map[int]*Equipment // All weapons / equipment the player is currently carrying. See also Weapons().
@@ -33,7 +33,7 @@ type Player struct {
3333
IsPlanting bool
3434
IsReloading bool
3535
IsUnknown bool // Used to identify unknown/broken players. see https://github.com/markus-wa/demoinfocs-golang/issues/162
36-
PreviousFramePosition r3.Vector // CS2 only, used to compute velocity as it's not networked in CS2 demos
36+
PreviousFramePosition r3.Vector // Deprecated: may be removed in v5 due to performance concerns, track this yourself.
3737
}
3838

3939
func (p *Player) PlayerPawnEntity() st.Entity {
@@ -85,9 +85,11 @@ func (p *Player) IsAlive() bool {
8585
}
8686

8787
if p.demoInfoProvider.IsSource2() {
88-
if pawnEntity := p.PlayerPawnEntity(); pawnEntity != nil {
88+
pawnEntity := p.PlayerPawnEntity()
89+
if pawnEntity != nil {
8990
return pawnEntity.PropertyValueMust("m_lifeState").S2UInt64() == 0
9091
}
92+
9193
return getBool(p.Entity, "m_bPawnIsAlive")
9294
}
9395

@@ -535,6 +537,7 @@ func (p *Player) PositionEyes() r3.Vector {
535537
}
536538

537539
// Velocity returns the player's velocity.
540+
// Deprecated: will be removed due to performance concerns, you will need to track this yourself.
538541
func (p *Player) Velocity() r3.Vector {
539542
if p.demoInfoProvider.IsSource2() {
540543
t := 64.0

pkg/demoinfocs/datatables.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package demoinfocs
33
import (
44
"fmt"
55
"math"
6+
"os"
67
"strings"
78

89
"github.com/golang/geo/r3"
@@ -637,6 +638,7 @@ func (p *parser) bindNewPlayerPawnS2(pawnEntity st.Entity) {
637638
if pl == nil {
638639
return
639640
}
641+
640642
if pl.IsAlive() {
641643
pl.LastAlivePosition = pos
642644
}
@@ -919,7 +921,7 @@ func (p *parser) bindGrenadeProjectiles(entity st.Entity) {
919921
if exists {
920922
wep = weaponType
921923
} else {
922-
fmt.Printf("unknown grenade model %d\n", model)
924+
fmt.Fprintf(os.Stderr, "unknown grenade model %d\n", model)
923925
}
924926
}
925927
}

pkg/demoinfocs/demoinfocs_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ func TestConcurrent(t *testing.T) {
493493
func parseDefaultDemo(tb testing.TB) {
494494
tb.Helper()
495495

496-
f := openFile(tb, defaultDemPath)
496+
f := openFile(tb, s2DemPath)
497497
defer mustClose(tb, f)
498498

499499
p := demoinfocs.NewParser(f)
@@ -599,15 +599,15 @@ func BenchmarkDemoInfoCs(b *testing.B) {
599599
}
600600

601601
func BenchmarkInMemory(b *testing.B) {
602-
f := openFile(b, defaultDemPath)
602+
f := openFile(b, s2DemPath)
603603
defer mustClose(b, f)
604604

605605
inf, err := f.Stat()
606-
assert.NoError(b, err, "failed to stat file %q", defaultDemPath)
606+
assert.NoError(b, err, "failed to stat file %q", s2DemPath)
607607

608608
d := make([]byte, inf.Size())
609609
n, err := f.Read(d)
610-
assert.NoError(b, err, "failed to read file %q", defaultDemPath)
610+
assert.NoError(b, err, "failed to read file %q", s2DemPath)
611611
assert.Equal(b, int64(n), inf.Size(), "byte count not as expected")
612612

613613
b.ResetTimer()

pkg/demoinfocs/parser.go

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ type parser struct {
104104
equipmentTypePerModel map[uint64]common.EquipmentType // Used to retrieve the EquipmentType of grenade projectiles based on models value. Source 2 only.
105105
stringTables []createStringTable // Contains all created sendtables, needed when updating them
106106
delayedEventHandlers []func() // Contains event handlers that need to be executed at the end of a tick (e.g. flash events because FlashDuration isn't updated before that)
107+
pendingMessagesCache []pendingMessage // Cache for pending messages that need to be dispatched after the current tick
107108
}
108109

109110
// NetMessageCreator creates additional net-messages to be dispatched to net-message handlers.

pkg/demoinfocs/s2_commands.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -310,21 +310,21 @@ func (p *parser) handleDemoPacket(pack *msgs2.CDemoPacket) {
310310

311311
r := bitread.NewSmallBitReader(bytes.NewReader(b))
312312

313-
ms := make([]pendingMessage, 0)
313+
p.pendingMessagesCache = p.pendingMessagesCache[:0]
314314

315315
for len(b)*8-r.ActualPosition() > 7 {
316316
t := int32(r.ReadUBitInt())
317317
size := r.ReadVarInt32()
318318
buf := r.ReadBytes(int(size))
319319

320-
ms = append(ms, pendingMessage{t, buf})
320+
p.pendingMessagesCache = append(p.pendingMessagesCache, pendingMessage{t, buf})
321321
}
322322

323-
sort.SliceStable(ms, func(i, j int) bool {
324-
return ms[i].priority() < ms[j].priority() // TODO: taken from dotabuff/manta. do we really need this?
323+
sort.SliceStable(p.pendingMessagesCache, func(i, j int) bool {
324+
return p.pendingMessagesCache[i].priority() < p.pendingMessagesCache[j].priority()
325325
})
326326

327-
for _, m := range ms {
327+
for _, m := range p.pendingMessagesCache {
328328
var msgCreator NetMessageCreator
329329

330330
if m.t < int32(msgs2.SVC_Messages_svc_ServerInfo) {

pkg/demoinfocs/sendtables2/entity.go

+8-21
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,9 @@ func (p *Parser) FilterEntity(fb func(*Entity) bool) []*Entity {
424424
}
425425

426426
func (e *Entity) readFields(r *reader, paths *[]*fieldPath) {
427-
readFieldPaths(r, paths)
427+
n := readFieldPaths(r, paths)
428428

429-
for _, fp := range *paths {
429+
for _, fp := range (*paths)[:n] {
430430
f := e.class.serializer.getFieldForFieldPath(fp, 0)
431431
name := e.class.getNameForFieldPath(fp)
432432
decoder, base := e.class.serializer.getDecoderForFieldPath2(fp, 0)
@@ -462,8 +462,6 @@ func (e *Entity) readFields(r *reader, paths *[]*fieldPath) {
462462
S2: true,
463463
})
464464
}
465-
466-
fp.release()
467465
}
468466
}
469467

@@ -486,15 +484,7 @@ func (p *Parser) OnPacketEntities(m *msgs2.CSVCMsg_PacketEntities) error {
486484
p.entityFullPackets++
487485
}
488486

489-
type tuple struct {
490-
ent *Entity
491-
op st.EntityOp
492-
}
493-
494-
var (
495-
tuples []tuple
496-
paths = make([]*fieldPath, 0)
497-
)
487+
p.tuplesCache = p.tuplesCache[:0]
498488

499489
for ; updates > 0; updates-- {
500490
var (
@@ -530,12 +520,10 @@ func (p *Parser) OnPacketEntities(m *msgs2.CSVCMsg_PacketEntities) error {
530520

531521
if baseline != nil {
532522
// POV demos are missing some baselines?
533-
e.readFields(newReader(baseline), &paths)
534-
paths = paths[:0]
523+
e.readFields(newReader(baseline), &p.pathCache)
535524
}
536525

537-
e.readFields(r, &paths)
538-
paths = paths[:0]
526+
e.readFields(r, &p.pathCache)
539527

540528
// Fire created-handlers so update-handlers can be registered
541529
for _, h := range class.createdHandlers {
@@ -559,8 +547,7 @@ func (p *Parser) OnPacketEntities(m *msgs2.CSVCMsg_PacketEntities) error {
559547
op |= st.EntityOpEntered
560548
}
561549

562-
e.readFields(r, &paths)
563-
paths = paths[:0]
550+
e.readFields(r, &p.pathCache)
564551
}
565552
} else {
566553
e = p.entities[index]
@@ -583,10 +570,10 @@ func (p *Parser) OnPacketEntities(m *msgs2.CSVCMsg_PacketEntities) error {
583570
}
584571
}
585572

586-
tuples = append(tuples, tuple{e, op})
573+
p.tuplesCache = append(p.tuplesCache, tuple{e, op})
587574
}
588575

589-
for _, t := range tuples {
576+
for _, t := range p.tuplesCache {
590577
e := t.ent
591578

592579
for _, h := range p.entityHandlers {

pkg/demoinfocs/sendtables2/field_decoder.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ func quantizedFactory(f *field) fieldDecoder {
260260
}
261261

262262
qfd := newQuantizedFloatDecoder(f.bitCount, f.encodeFlags, f.lowValue, f.highValue)
263+
263264
return func(r *reader) interface{} {
264265
return qfd.decode(r)
265266
}
@@ -274,9 +275,11 @@ func vectorFactory(n int) fieldFactory {
274275
d := floatFactory(f)
275276
return func(r *reader) interface{} {
276277
x := make([]float32, n)
278+
277279
for i := 0; i < n; i++ {
278280
x[i] = d(r).(float32)
279281
}
282+
280283
return x
281284
}
282285
}
@@ -319,7 +322,7 @@ func ammoDecoder(r *reader) interface{} {
319322
}
320323

321324
func noscaleDecoder(r *reader) interface{} {
322-
return math.Float32frombits(r.readBits(32))
325+
return math.Float32frombits(r.readLeUint32())
323326
}
324327

325328
func runeTimeDecoder(r *reader) interface{} {

pkg/demoinfocs/sendtables2/field_path.go

+15-3
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,10 @@ func (fp *fieldPath) release() {
306306
}
307307

308308
// readFieldPaths reads a new slice of fieldPath values from the given reader
309-
func readFieldPaths(r *reader, paths *[]*fieldPath) {
309+
func readFieldPaths(r *reader, paths *[]*fieldPath) int {
310310
fp := newFieldPath()
311-
312311
node := huffTree
312+
i := 0
313313

314314
for !fp.done {
315315
var next huffmanTree
@@ -326,14 +326,26 @@ func readFieldPaths(r *reader, paths *[]*fieldPath) {
326326
fieldPathTable[next.Value()].fn(r, fp)
327327

328328
if !fp.done {
329-
*paths = append(*paths, fp.copy())
329+
if len(*paths) <= i {
330+
*paths = append(*paths, fp.copy())
331+
} else {
332+
x := (*paths)[i]
333+
x.last = fp.last
334+
x.done = fp.done
335+
336+
copy(x.path, fp.path)
337+
}
338+
339+
i++
330340
}
331341
} else {
332342
node = next
333343
}
334344
}
335345

336346
fp.release()
347+
348+
return i
337349
}
338350

339351
// newHuffmanTree creates a new huffmanTree from the field path table

pkg/demoinfocs/sendtables2/field_state.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ func (s *fieldState) get(fp *fieldPath) interface{} {
3232
func (s *fieldState) set(fp *fieldPath, v interface{}) {
3333
x := s
3434
z := 0
35+
3536
for i := 0; i <= fp.last; i++ {
3637
z = fp.path[i]
38+
3739
if y := len(x.state); y <= z {
3840
newCap := max(z+2, y*2)
39-
if newCap > cap(x.state) {
41+
if z+2 > cap(x.state) {
4042
newSlice := make([]interface{}, z+1, newCap)
4143
copy(newSlice, x.state)
4244
x.state = newSlice
@@ -45,15 +47,18 @@ func (s *fieldState) set(fp *fieldPath, v interface{}) {
4547
x.state = x.state[:z+1]
4648
}
4749
}
50+
4851
if i == fp.last {
4952
if _, ok := x.state[z].(*fieldState); !ok {
5053
x.state[z] = v
5154
}
5255
return
5356
}
57+
5458
if _, ok := x.state[z].(*fieldState); !ok {
5559
x.state[z] = newFieldState()
5660
}
61+
5762
x = x.state[z].(*fieldState)
5863
}
5964
}

pkg/demoinfocs/sendtables2/parser.go

+7
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ var itemCounts = map[string]int{
5151
"MAX_ABILITY_DRAFT_ABILITIES": 48,
5252
}
5353

54+
type tuple struct {
55+
ent *Entity
56+
op st.EntityOp
57+
}
58+
5459
type Parser struct {
5560
serializers map[string]*serializer
5661
classIdSize uint32
@@ -60,6 +65,8 @@ type Parser struct {
6065
entityFullPackets int
6166
entities map[int32]*Entity
6267
entityHandlers []st.EntityHandler
68+
pathCache []*fieldPath
69+
tuplesCache []tuple
6370
}
6471

6572
func (p *Parser) ReadEnterPVS(r *bit.BitReader, index int, entities map[int]st.Entity, slot int) st.Entity {

pkg/demoinfocs/sendtables2/reader.go

+16-5
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ func (r *reader) remBytes() uint32 {
3939

4040
// nextByte reads the next byte from the buffer
4141
func (r *reader) nextByte() byte {
42-
r.pos++
43-
44-
if r.pos > r.size {
42+
if r.pos >= r.size {
4543
_panicf("nextByte: insufficient buffer (%d of %d)", r.pos, r.size)
4644
}
4745

48-
return r.buf[r.pos-1]
46+
x := r.buf[r.pos]
47+
48+
r.pos++
49+
50+
return x
4951
}
5052

5153
// readBits returns the uint32 value for the given number of sequential bits
@@ -77,22 +79,31 @@ func (r *reader) readBytes(n uint32) []byte {
7779
// Fast path if we're byte aligned
7880
if r.bitCount == 0 {
7981
r.pos += n
82+
8083
if r.pos > r.size {
8184
_panicf("readBytes: insufficient buffer (%d of %d)", r.pos, r.size)
8285
}
86+
8387
return r.buf[r.pos-n : r.pos]
8488
}
8589

8690
buf := make([]byte, n)
91+
8792
for i := uint32(0); i < n; i++ {
8893
buf[i] = byte(r.readBits(8))
8994
}
95+
9096
return buf
9197
}
9298

9399
// readLeUint32 reads an little-endian uint32
94100
func (r *reader) readLeUint32() uint32 {
95-
return binary.LittleEndian.Uint32(r.readBytes(4))
101+
// Fast path if we're byte aligned
102+
if r.bitCount == 0 {
103+
return binary.LittleEndian.Uint32(r.readBytes(4))
104+
}
105+
106+
return r.readBits(32)
96107
}
97108

98109
// readLeUint64 reads a little-endian uint64

0 commit comments

Comments
 (0)