github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/symdb/block_writer_v2.go (about)

     1  package symdb
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"hash/crc32"
     7  	"io"
     8  	"math"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/grafana/dskit/multierror"
    13  	"github.com/parquet-go/parquet-go"
    14  	"golang.org/x/sync/errgroup"
    15  
    16  	"github.com/grafana/pyroscope/pkg/phlaredb/block"
    17  	schemav1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1"
    18  	"github.com/grafana/pyroscope/pkg/util/build"
    19  )
    20  
    21  type writerV2 struct {
    22  	config *Config
    23  
    24  	index       IndexFile
    25  	indexWriter *fileWriter
    26  	stacktraces *fileWriter
    27  	files       []block.File
    28  
    29  	// Parquet tables.
    30  	mappings  parquetWriter[schemav1.InMemoryMapping, schemav1.MappingPersister]
    31  	functions parquetWriter[schemav1.InMemoryFunction, schemav1.FunctionPersister]
    32  	locations parquetWriter[schemav1.InMemoryLocation, schemav1.LocationPersister]
    33  	strings   parquetWriter[string, schemav1.StringPersister]
    34  }
    35  
    36  func newWriterV2(c *Config) *writerV2 {
    37  	return &writerV2{
    38  		config: c,
    39  		index: IndexFile{
    40  			Header: IndexHeader{
    41  				Magic:   symdbMagic,
    42  				Version: FormatV2,
    43  			},
    44  		},
    45  	}
    46  }
    47  
    48  func (w *writerV2) writePartitions(partitions []*PartitionWriter) error {
    49  	if err := w.createDir(); err != nil {
    50  		return err
    51  	}
    52  
    53  	g, _ := errgroup.WithContext(context.Background())
    54  	g.Go(func() (err error) {
    55  		if w.stacktraces, err = w.newFile(StacktracesFileName); err != nil {
    56  			return err
    57  		}
    58  		for _, partition := range partitions {
    59  			if err = w.writeStacktraces(partition); err != nil {
    60  				return err
    61  			}
    62  		}
    63  		return w.stacktraces.Close()
    64  	})
    65  
    66  	g.Go(func() (err error) {
    67  		if err = w.strings.init(w.config.Dir, w.config.Parquet); err != nil {
    68  			return err
    69  		}
    70  		for _, partition := range partitions {
    71  			if partition.header.V2.Strings, err = w.strings.readFrom(partition.strings.slice); err != nil {
    72  				return err
    73  			}
    74  		}
    75  		return w.strings.Close()
    76  	})
    77  
    78  	g.Go(func() (err error) {
    79  		if err = w.functions.init(w.config.Dir, w.config.Parquet); err != nil {
    80  			return err
    81  		}
    82  		for _, partition := range partitions {
    83  			if partition.header.V2.Functions, err = w.functions.readFrom(partition.functions.slice); err != nil {
    84  				return err
    85  			}
    86  		}
    87  		return w.functions.Close()
    88  	})
    89  
    90  	g.Go(func() (err error) {
    91  		if err = w.mappings.init(w.config.Dir, w.config.Parquet); err != nil {
    92  			return err
    93  		}
    94  		for _, partition := range partitions {
    95  			if partition.header.V2.Mappings, err = w.mappings.readFrom(partition.mappings.slice); err != nil {
    96  				return err
    97  			}
    98  		}
    99  		return w.mappings.Close()
   100  	})
   101  
   102  	g.Go(func() (err error) {
   103  		if err = w.locations.init(w.config.Dir, w.config.Parquet); err != nil {
   104  			return err
   105  		}
   106  		for _, partition := range partitions {
   107  			if partition.header.V2.Locations, err = w.locations.readFrom(partition.locations.slice); err != nil {
   108  				return err
   109  			}
   110  		}
   111  		return w.locations.Close()
   112  	})
   113  
   114  	if err := g.Wait(); err != nil {
   115  		return err
   116  	}
   117  
   118  	for _, partition := range partitions {
   119  		w.index.PartitionHeaders = append(w.index.PartitionHeaders, &partition.header)
   120  	}
   121  
   122  	return w.Flush()
   123  }
   124  
   125  func (w *writerV2) Flush() (err error) {
   126  	if err = w.writeIndexFile(); err != nil {
   127  		return err
   128  	}
   129  	w.files = []block.File{
   130  		w.indexWriter.meta(),
   131  		w.stacktraces.meta(),
   132  		w.locations.meta(),
   133  		w.mappings.meta(),
   134  		w.functions.meta(),
   135  		w.strings.meta(),
   136  	}
   137  	return nil
   138  }
   139  
   140  func (w *writerV2) writeStacktraces(partition *PartitionWriter) (err error) {
   141  	h := StacktraceBlockHeader{
   142  		Offset:             w.stacktraces.w.offset,
   143  		Partition:          partition.header.Partition,
   144  		Encoding:           StacktraceEncodingGroupVarint,
   145  		Stacktraces:        uint32(len(partition.stacktraces.hashToIdx)),
   146  		StacktraceNodes:    partition.stacktraces.tree.len(),
   147  		StacktraceMaxNodes: math.MaxUint32,
   148  	}
   149  	crc := crc32.New(castagnoli)
   150  	if h.Size, err = partition.stacktraces.WriteTo(io.MultiWriter(crc, w.stacktraces)); err != nil {
   151  		return fmt.Errorf("writing stacktrace chunk data: %w", err)
   152  	}
   153  	h.CRC = crc.Sum32()
   154  	partition.header.Stacktraces = append(partition.header.Stacktraces, h)
   155  	return nil
   156  }
   157  
   158  func (w *writerV2) createDir() error {
   159  	if err := os.MkdirAll(w.config.Dir, 0o755); err != nil {
   160  		return fmt.Errorf("failed to create directory %q: %w", w.config.Dir, err)
   161  	}
   162  	return nil
   163  }
   164  
   165  func (w *writerV2) writeIndexFile() (err error) {
   166  	// Write the index file only after all the files were flushed.
   167  	if w.indexWriter, err = w.newFile(IndexFileName); err != nil {
   168  		return err
   169  	}
   170  	defer func() {
   171  		err = multierror.New(err, w.indexWriter.Close()).Err()
   172  	}()
   173  	if _, err = w.index.WriteTo(w.indexWriter); err != nil {
   174  		return fmt.Errorf("failed to write index file: %w", err)
   175  	}
   176  	return err
   177  }
   178  
   179  func (w *writerV2) newFile(path string) (f *fileWriter, err error) {
   180  	path = filepath.Join(w.config.Dir, path)
   181  	if f, err = newFileWriter(path); err != nil {
   182  		return nil, fmt.Errorf("failed to create %q: %w", path, err)
   183  	}
   184  	return f, err
   185  }
   186  
   187  func (w *writerV2) meta() []block.File { return w.files }
   188  
   189  type parquetWriter[M schemav1.Models, P schemav1.Persister[M]] struct {
   190  	persister P
   191  	config    ParquetConfig
   192  
   193  	currentRowGroup uint32
   194  	currentRows     uint32
   195  	rowsTotal       uint64
   196  
   197  	buffer    *parquet.Buffer
   198  	rowsBatch []parquet.Row
   199  
   200  	writer *parquet.GenericWriter[P]
   201  	file   *os.File
   202  	path   string
   203  }
   204  
   205  func (s *parquetWriter[M, P]) init(dir string, c ParquetConfig) (err error) {
   206  	s.config = c
   207  	s.path = filepath.Join(dir, s.persister.Name()+block.ParquetSuffix)
   208  	s.file, err = os.OpenFile(s.path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	s.rowsBatch = make([]parquet.Row, 0, 128)
   213  	s.buffer = parquet.NewBuffer(s.persister.Schema())
   214  	s.writer = parquet.NewGenericWriter[P](s.file, s.persister.Schema(),
   215  		parquet.CreatedBy("github.com/grafana/pyroscope/", build.Version, build.Revision),
   216  		parquet.PageBufferSize(3*1024*1024),
   217  	)
   218  	return nil
   219  }
   220  
   221  func (s *parquetWriter[M, P]) readFrom(values []M) (ranges []RowRangeReference, err error) {
   222  	for len(values) > 0 {
   223  		var r RowRangeReference
   224  		if r, err = s.writeRows(values); err != nil {
   225  			return nil, err
   226  		}
   227  		ranges = append(ranges, r)
   228  		values = values[r.Rows:]
   229  	}
   230  	return ranges, nil
   231  }
   232  
   233  func (s *parquetWriter[M, P]) writeRows(values []M) (r RowRangeReference, err error) {
   234  	r.RowGroup = s.currentRowGroup
   235  	r.Index = s.currentRows
   236  	if len(values) == 0 {
   237  		return r, nil
   238  	}
   239  	var n int
   240  	for len(values) > 0 && int(s.currentRows) < s.config.MaxBufferRowCount {
   241  		s.fillBatch(values)
   242  		if n, err = s.buffer.WriteRows(s.rowsBatch); err != nil {
   243  			return r, err
   244  		}
   245  		s.currentRows += uint32(n)
   246  		r.Rows += uint32(n)
   247  		values = values[n:]
   248  	}
   249  	if int(s.currentRows)+cap(s.rowsBatch) >= s.config.MaxBufferRowCount {
   250  		if err = s.flushBuffer(); err != nil {
   251  			return r, err
   252  		}
   253  	}
   254  	return r, nil
   255  }
   256  
   257  func (s *parquetWriter[M, P]) fillBatch(values []M) int {
   258  	m := min(len(values), cap(s.rowsBatch))
   259  	s.rowsBatch = s.rowsBatch[:m]
   260  	for i := 0; i < m; i++ {
   261  		row := s.rowsBatch[i][:0]
   262  		s.rowsBatch[i] = s.persister.Deconstruct(row, values[i])
   263  	}
   264  	return m
   265  }
   266  
   267  func (s *parquetWriter[M, P]) flushBuffer() error {
   268  	if _, err := s.writer.WriteRowGroup(s.buffer); err != nil {
   269  		return err
   270  	}
   271  	s.rowsTotal += uint64(s.buffer.NumRows())
   272  	s.currentRowGroup++
   273  	s.currentRows = 0
   274  	s.buffer.Reset()
   275  	return nil
   276  }
   277  
   278  func (s *parquetWriter[M, P]) meta() block.File {
   279  	f := block.File{
   280  		// Note that the path is relative to the symdb root dir.
   281  		RelPath: filepath.Base(s.path),
   282  		Parquet: &block.ParquetFile{
   283  			NumRows: s.rowsTotal,
   284  		},
   285  	}
   286  	if f.Parquet.NumRows > 0 {
   287  		f.Parquet.NumRowGroups = uint64(s.currentRowGroup + 1)
   288  	}
   289  	if stat, err := os.Stat(s.path); err == nil {
   290  		f.SizeBytes = uint64(stat.Size())
   291  	}
   292  	return f
   293  }
   294  
   295  func (s *parquetWriter[M, P]) Close() error {
   296  	if err := s.flushBuffer(); err != nil {
   297  		return fmt.Errorf("flushing parquet buffer: %w", err)
   298  	}
   299  	if err := s.writer.Close(); err != nil {
   300  		return fmt.Errorf("closing parquet writer: %w", err)
   301  	}
   302  	if err := s.file.Close(); err != nil {
   303  		return fmt.Errorf("closing parquet file: %w", err)
   304  	}
   305  	return nil
   306  }