mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
8d968acd0a
This improves performance of `field_values` pipe when it is applied to large number of data blocks. This also improves performance of /select/logsql/field_values HTTP API.
239 lines
5.5 KiB
Go
239 lines
5.5 KiB
Go
package logstorage
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unsafe"
|
|
)
|
|
|
|
// pipeFieldNames processes '| field_names' pipe.
|
|
//
|
|
// See https://docs.victoriametrics.com/victorialogs/logsql/#field_names-pipe
|
|
type pipeFieldNames struct {
|
|
// resultName is an optional name of the column to write results to.
|
|
// By default results are written into 'name' column.
|
|
resultName string
|
|
|
|
// if isFirstPipe is set, then there is no need in loading columnsHeader in writeBlock().
|
|
isFirstPipe bool
|
|
}
|
|
|
|
func (pf *pipeFieldNames) String() string {
|
|
s := "field_names"
|
|
if pf.resultName != "name" {
|
|
s += " as " + quoteTokenIfNeeded(pf.resultName)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (pf *pipeFieldNames) canLiveTail() bool {
|
|
return false
|
|
}
|
|
|
|
func (pf *pipeFieldNames) updateNeededFields(neededFields, unneededFields fieldsSet) {
|
|
if pf.isFirstPipe {
|
|
neededFields.reset()
|
|
} else {
|
|
neededFields.add("*")
|
|
}
|
|
unneededFields.reset()
|
|
}
|
|
|
|
func (pf *pipeFieldNames) optimize() {
|
|
// nothing to do
|
|
}
|
|
|
|
func (pf *pipeFieldNames) hasFilterInWithQuery() bool {
|
|
return false
|
|
}
|
|
|
|
func (pf *pipeFieldNames) initFilterInValues(_ map[string][]string, _ getFieldValuesFunc) (pipe, error) {
|
|
return pf, nil
|
|
}
|
|
|
|
func (pf *pipeFieldNames) newPipeProcessor(workersCount int, stopCh <-chan struct{}, _ func(), ppNext pipeProcessor) pipeProcessor {
|
|
shards := make([]pipeFieldNamesProcessorShard, workersCount)
|
|
|
|
pfp := &pipeFieldNamesProcessor{
|
|
pf: pf,
|
|
stopCh: stopCh,
|
|
ppNext: ppNext,
|
|
|
|
shards: shards,
|
|
}
|
|
return pfp
|
|
}
|
|
|
|
type pipeFieldNamesProcessor struct {
|
|
pf *pipeFieldNames
|
|
stopCh <-chan struct{}
|
|
ppNext pipeProcessor
|
|
|
|
shards []pipeFieldNamesProcessorShard
|
|
}
|
|
|
|
type pipeFieldNamesProcessorShard struct {
|
|
pipeFieldNamesProcessorShardNopad
|
|
|
|
// The padding prevents false sharing on widespread platforms with 128 mod (cache line size) = 0 .
|
|
_ [128 - unsafe.Sizeof(pipeFieldNamesProcessorShardNopad{})%128]byte
|
|
}
|
|
|
|
type pipeFieldNamesProcessorShardNopad struct {
|
|
// m holds hits per each field name
|
|
m map[string]*uint64
|
|
}
|
|
|
|
func (shard *pipeFieldNamesProcessorShard) getM() map[string]*uint64 {
|
|
if shard.m == nil {
|
|
shard.m = make(map[string]*uint64)
|
|
}
|
|
return shard.m
|
|
}
|
|
|
|
func (pfp *pipeFieldNamesProcessor) writeBlock(workerID uint, br *blockResult) {
|
|
if br.rowsLen == 0 {
|
|
return
|
|
}
|
|
|
|
// Assume that the column is set for all the rows in the block.
|
|
// This is much faster than reading all the column values and counting non-empty rows.
|
|
hits := uint64(br.rowsLen)
|
|
|
|
shard := &pfp.shards[workerID]
|
|
if !pfp.pf.isFirstPipe || br.bs == nil || br.bs.partFormatVersion() < 1 {
|
|
cs := br.getColumns()
|
|
for _, c := range cs {
|
|
shard.updateColumnHits(c.name, hits)
|
|
}
|
|
} else {
|
|
cshIndex := br.bs.getColumnsHeaderIndex()
|
|
shard.updateHits(cshIndex.columnHeadersRefs, br, hits)
|
|
shard.updateHits(cshIndex.constColumnsRefs, br, hits)
|
|
shard.updateColumnHits("_time", hits)
|
|
shard.updateColumnHits("_stream", hits)
|
|
shard.updateColumnHits("_stream_id", hits)
|
|
}
|
|
}
|
|
|
|
func (shard *pipeFieldNamesProcessorShard) updateHits(refs []columnHeaderRef, br *blockResult, hits uint64) {
|
|
for _, cr := range refs {
|
|
columnName := br.bs.getColumnNameByID(cr.columnNameID)
|
|
shard.updateColumnHits(columnName, hits)
|
|
}
|
|
}
|
|
|
|
func (shard *pipeFieldNamesProcessorShard) updateColumnHits(columnName string, hits uint64) {
|
|
if columnName == "" {
|
|
columnName = "_msg"
|
|
}
|
|
m := shard.getM()
|
|
pHits := m[columnName]
|
|
if pHits == nil {
|
|
nameCopy := strings.Clone(columnName)
|
|
hits := uint64(0)
|
|
pHits = &hits
|
|
m[nameCopy] = pHits
|
|
}
|
|
*pHits += hits
|
|
}
|
|
|
|
func (pfp *pipeFieldNamesProcessor) flush() error {
|
|
if needStop(pfp.stopCh) {
|
|
return nil
|
|
}
|
|
|
|
// merge state across shards
|
|
shards := pfp.shards
|
|
m := shards[0].getM()
|
|
shards = shards[1:]
|
|
for i := range shards {
|
|
for name, pHitsSrc := range shards[i].getM() {
|
|
pHits := m[name]
|
|
if pHits == nil {
|
|
m[name] = pHitsSrc
|
|
} else {
|
|
*pHits += *pHitsSrc
|
|
}
|
|
}
|
|
}
|
|
|
|
// write result
|
|
wctx := &pipeFieldNamesWriteContext{
|
|
pfp: pfp,
|
|
}
|
|
wctx.rcs[0].name = pfp.pf.resultName
|
|
wctx.rcs[1].name = "hits"
|
|
|
|
for name, pHits := range m {
|
|
hits := string(marshalUint64String(nil, *pHits))
|
|
wctx.writeRow(name, hits)
|
|
}
|
|
wctx.flush()
|
|
|
|
return nil
|
|
}
|
|
|
|
type pipeFieldNamesWriteContext struct {
|
|
pfp *pipeFieldNamesProcessor
|
|
rcs [2]resultColumn
|
|
br blockResult
|
|
|
|
// rowsCount is the number of rows in the current block
|
|
rowsCount int
|
|
|
|
// valuesLen is the total length of values in the current block
|
|
valuesLen int
|
|
}
|
|
|
|
func (wctx *pipeFieldNamesWriteContext) writeRow(name, hits string) {
|
|
wctx.rcs[0].addValue(name)
|
|
wctx.rcs[1].addValue(hits)
|
|
wctx.valuesLen += len(name) + len(hits)
|
|
wctx.rowsCount++
|
|
if wctx.valuesLen >= 1_000_000 {
|
|
wctx.flush()
|
|
}
|
|
}
|
|
|
|
func (wctx *pipeFieldNamesWriteContext) flush() {
|
|
br := &wctx.br
|
|
|
|
wctx.valuesLen = 0
|
|
|
|
// Flush rcs to ppNext
|
|
br.setResultColumns(wctx.rcs[:], wctx.rowsCount)
|
|
wctx.rowsCount = 0
|
|
wctx.pfp.ppNext.writeBlock(0, br)
|
|
br.reset()
|
|
wctx.rcs[0].resetValues()
|
|
wctx.rcs[1].resetValues()
|
|
}
|
|
|
|
func parsePipeFieldNames(lex *lexer) (*pipeFieldNames, error) {
|
|
if !lex.isKeyword("field_names") {
|
|
return nil, fmt.Errorf("expecting 'field_names'; got %q", lex.token)
|
|
}
|
|
lex.nextToken()
|
|
|
|
resultName := "name"
|
|
if lex.isKeyword("as") {
|
|
lex.nextToken()
|
|
name, err := parseFieldName(lex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse result name for 'field_names': %w", err)
|
|
}
|
|
resultName = name
|
|
} else if !lex.isKeyword("", "|") {
|
|
name, err := parseFieldName(lex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse result name for 'field_names': %w", err)
|
|
}
|
|
resultName = name
|
|
}
|
|
|
|
pf := &pipeFieldNames{
|
|
resultName: resultName,
|
|
}
|
|
return pf, nil
|
|
}
|