• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codenotary / immudb / 25360351619

04 May 2026 04:37PM UTC coverage: 84.989% (-0.3%) from 85.255%
25360351619

Pull #2094

gh-ci

vchaindz
perf: hot-path allocation and lock-contention fixes

Seven independent improvements driven from a manual review of
allocations and lock-holding patterns in the read/write hot paths.
Each fix is self-contained and on-disk format-neutral.

P1 — sync() vLog lock convoy
embedded/store/immustore.go (sync)

  The for-range over s.vLogs called defer s.releaseVLog(i+1) inside
  the loop. Go stacks defers until the enclosing function returns,
  so all per-vLog locks were held for the full duration of every
  vLog.Flush + vLog.Sync of every other vLog, blocking concurrent
  readers on vLogsCond for the entire sync window. Replaced the
  defer with a per-iteration IIFE so each vLog drops its lock as
  soon as its own flush+sync completes. No reordering of the actual
  flush/sync calls; only the lock-release timing changes.

P2 — zero-copy snapshot reads
embedded/tbtree/{tbtree,snapshot}.go

  TBtree.Get and Snapshot.Get always finish with `return cp(v), …`
  where cp performs make+copy of the value bytes — even on cache
  hits served entirely from already-immutable in-memory leafValue
  slices. The defensive copy exists only to protect against caller
  mutation. Added GetReadonly companions next to each (additive
  API, no behaviour change for existing callers): same body minus
  the cp, with a doc-comment that the returned slice is borrowed
  and must not be mutated. Migrating callers is intentionally a
  separate audit pass — every callsite needs a hand-check before
  switching.

P4 — per-tx scratch buffer in commit loops
embedded/store/immustore.go (commitDurable, sync)

  Both commit loops allocated `cb := make([]byte, s.cLogEntrySize)`
  inside the loop body, once per transaction in a batch. cLog.Append
  ultimately routes to AppendableFile.write, which copies the input
  into its own writeBuffer (singleapp/single_app.go:477), so the
  input slice is not retained — safe to hoist a single 12- or
  44-byte buffer outside the loop and reuse it across iteration... (continued)
Pull Request #2094: perf(s3): wave 1-4 remote-storage performance + sql index-only COUNT + hot-path fixes

601 of 855 new or added lines in 16 files covered. (70.29%)

8 existing lines in 3 files now uncovered.

45131 of 53102 relevant lines covered (84.99%)

126741.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.14
/embedded/sql/count_row_reader.go
1
/*
2
Copyright 2026 Codenotary Inc. All rights reserved.
3

4
SPDX-License-Identifier: BUSL-1.1
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    https://mariadb.com/bsl11/
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package sql
18

19
import "context"
20

21
// countingRowReader handles bare COUNT(*) queries (SELECT COUNT(*) FROM tbl
22
// with no WHERE, JOINs, GROUP BY, or HAVING) without decoding column values.
23
// It wraps a rawRowReader and returns exactly one Row containing the count.
24
type countingRowReader struct {
25
        rawReader *rawRowReader
26
        // encSel is the encoded selector key expected by projectedRowReader, e.g.
27
        // "COUNT()(t.*)" for a table aliased as "t".
28
        encSel  string
29
        colDesc ColDescriptor
30
        done    bool
31
}
32

33
func newCountingRowReader(rawReader *rawRowReader, agg *AggColSelector) *countingRowReader {
47✔
34
        aggFn, table, col := agg.resolve(rawReader.tableAlias)
47✔
35
        encSel := EncodeSelector(aggFn, table, col)
47✔
36
        return &countingRowReader{
47✔
37
                rawReader: rawReader,
47✔
38
                encSel:    encSel,
47✔
39
                colDesc: ColDescriptor{
47✔
40
                        AggFn:  aggFn,
47✔
41
                        Table:  table,
47✔
42
                        Column: col,
47✔
43
                        Type:   IntegerType,
47✔
44
                },
47✔
45
        }
47✔
46
}
47✔
47

48
func (cr *countingRowReader) onClose(callback func()) {
35✔
49
        cr.rawReader.onClose(callback)
35✔
50
}
35✔
51

52
func (cr *countingRowReader) Tx() *SQLTx {
40✔
53
        return cr.rawReader.Tx()
40✔
54
}
40✔
55

56
func (cr *countingRowReader) TableAlias() string {
232✔
57
        return cr.rawReader.TableAlias()
232✔
58
}
232✔
59

60
func (cr *countingRowReader) Parameters() map[string]interface{} {
32✔
61
        return cr.rawReader.Parameters()
32✔
62
}
32✔
63

64
func (cr *countingRowReader) OrderBy() []ColDescriptor {
×
65
        return nil
×
66
}
×
67

68
func (cr *countingRowReader) ScanSpecs() *ScanSpecs {
×
69
        return cr.rawReader.ScanSpecs()
×
70
}
×
71

72
func (cr *countingRowReader) Columns(_ context.Context) ([]ColDescriptor, error) {
×
73
        return []ColDescriptor{cr.colDesc}, nil
×
74
}
×
75

76
// colsBySelector includes both the COUNT(*) aggregation descriptor and all
77
// raw table column descriptors so that projectedRowReader validation succeeds.
78
func (cr *countingRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) {
32✔
79
        colsBySel, err := cr.rawReader.colsBySelector(ctx)
32✔
80
        if err != nil {
32✔
81
                return nil, err
×
82
        }
×
83
        colsBySel[cr.encSel] = cr.colDesc
32✔
84
        return colsBySel, nil
32✔
85
}
86

87
func (cr *countingRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error {
6✔
88
        return cr.rawReader.InferParameters(ctx, params)
6✔
89
}
6✔
90

91
// Read counts all matching index entries without decoding column values and
92
// returns a single Row. Subsequent calls return ErrNoMoreRows.
93
func (cr *countingRowReader) Read(ctx context.Context) (*Row, error) {
44✔
94
        if cr.done {
62✔
95
                return nil, ErrNoMoreRows
18✔
96
        }
18✔
97
        cr.done = true
26✔
98

26✔
99
        n, err := cr.rawReader.CountAll(ctx)
26✔
100
        if err != nil {
26✔
101
                return nil, err
×
102
        }
×
103

104
        val := &Integer{val: n}
26✔
105
        return &Row{
26✔
106
                ValuesByPosition: []TypedValue{val},
26✔
107
                ValuesBySelector: map[string]TypedValue{cr.encSel: val},
26✔
108
        }, nil
26✔
109
}
110

111
func (cr *countingRowReader) Close() error {
47✔
112
        return cr.rawReader.Close()
47✔
113
}
47✔
114

115
// keyFilterCountingRowReader extends the COUNT(*) fast-path to queries with a
116
// WHERE clause whose columns are all part of the chosen index. The predicate
117
// is evaluated against values decoded from the index key, so the row payload
118
// is never resolved or decoded — see rawRowReader.CountAllWithKeyFilter.
119
type keyFilterCountingRowReader struct {
120
        *countingRowReader
121
        where ValueExp
122
}
123

124
func newKeyFilterCountingRowReader(rawReader *rawRowReader, agg *AggColSelector, where ValueExp) *keyFilterCountingRowReader {
6✔
125
        return &keyFilterCountingRowReader{
6✔
126
                countingRowReader: newCountingRowReader(rawReader, agg),
6✔
127
                where:             where,
6✔
128
        }
6✔
129
}
6✔
130

131
// Read evaluates the index-only WHERE filter while counting entries, returning
132
// a single Row with the resulting count. Subsequent calls return ErrNoMoreRows.
133
func (cr *keyFilterCountingRowReader) Read(ctx context.Context) (*Row, error) {
12✔
134
        if cr.done {
18✔
135
                return nil, ErrNoMoreRows
6✔
136
        }
6✔
137
        cr.done = true
6✔
138

6✔
139
        n, err := cr.rawReader.CountAllWithKeyFilter(ctx, cr.where)
6✔
140
        if err != nil {
6✔
NEW
141
                return nil, err
×
NEW
142
        }
×
143

144
        val := &Integer{val: n}
6✔
145
        return &Row{
6✔
146
                ValuesByPosition: []TypedValue{val},
6✔
147
                ValuesBySelector: map[string]TypedValue{cr.encSel: val},
6✔
148
        }, nil
6✔
149
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc