浏览代码

Fix branches in Burndown analysis

Signed-off-by: Vadim Markovtsev <vadim@sourced.tech>
Vadim Markovtsev 6 年之前
父节点
当前提交
93e8d8a323
共有 4 个文件被更改,包括 231 次插入272 次删除
  1. 19 37
      internal/burndown/file.go
  2. 20 15
      internal/burndown/file_test.go
  3. 157 183
      leaves/burndown.go
  4. 35 37
      leaves/burndown_test.go

+ 19 - 37
internal/burndown/file.go

@@ -8,11 +8,8 @@ import (
 	"gopkg.in/src-d/hercules.v4/internal/rbtree"
 )
 
-// Status is the something we would like to keep track of in File.Update().
-type Status struct {
-	data   interface{}
-	update func(interface{}, int, int, int)
-}
+// Updater is the function which is called back on File.Update().
+type Updater = func(currentTime, previousTime, delta int)
 
 // File encapsulates a balanced binary tree to store line intervals and
 // a cumulative mapping of values to the corresponding length counters. Users
@@ -30,13 +27,7 @@ type File struct {
 	Hash     plumbing.Hash
 
 	tree     *rbtree.RBTree
-	statuses []Status
-}
-
-// NewStatus initializes a new instance of Status struct. It is needed to set the only two
-// private fields which are not supposed to be replaced during the whole lifetime.
-func NewStatus(data interface{}, update func(interface{}, int, int, int)) Status {
-	return Status{data: data, update: update}
+	updaters []Updater
 }
 
 // TreeEnd denotes the value of the last leaf in the tree.
@@ -47,13 +38,13 @@ const TreeMaxBinPower = 14
 // TreeMergeMark is the special day which disables the status updates and is used in File.Merge().
 const TreeMergeMark = (1 << TreeMaxBinPower) - 1
 
-func (file *File) updateTime(currentTime int, previousTime int, delta int) {
+func (file *File) updateTime(currentTime, previousTime, delta int) {
 	if currentTime & TreeMergeMark == TreeMergeMark {
 		// merge mode
 		return
 	}
-	for _, status := range file.statuses {
-		status.update(status.data, currentTime, previousTime, delta)
+	for _, update := range file.updaters {
+		update(currentTime, previousTime, delta)
 	}
 }
 
@@ -64,9 +55,9 @@ func (file *File) updateTime(currentTime int, previousTime int, delta int) {
 // length is the starting length of the tree (the key of the second and the
 // last node);
 //
-// statuses are the attached interval length mappings.
-func NewFile(hash plumbing.Hash, time int, length int, statuses ...Status) *File {
-	file := &File{Hash: hash, tree: new(rbtree.RBTree), statuses: statuses}
+// updaters are the attached interval length mappings.
+func NewFile(hash plumbing.Hash, time int, length int, updaters ...Updater) *File {
+	file := &File{Hash: hash, tree: new(rbtree.RBTree), updaters: updaters}
 	if length > 0 {
 		file.updateTime(time, time, length)
 		file.tree.Insert(rbtree.Item{Key: 0, Value: time})
@@ -82,9 +73,9 @@ func NewFile(hash plumbing.Hash, time int, length int, statuses ...Status) *File
 //
 // vals is a slice with the starting tree values. Must match the size of keys.
 //
-// statuses are the attached interval length mappings.
-func NewFileFromTree(hash plumbing.Hash, keys []int, vals []int, statuses ...Status) *File {
-	file := &File{Hash: hash, tree: new(rbtree.RBTree), statuses: statuses}
+// updaters are the attached interval length mappings.
+func NewFileFromTree(hash plumbing.Hash, keys []int, vals []int, updaters ...Updater) *File {
+	file := &File{Hash: hash, tree: new(rbtree.RBTree), updaters: updaters}
 	if len(keys) != len(vals) {
 		panic("keys and vals must be of equal length")
 	}
@@ -96,15 +87,15 @@ func NewFileFromTree(hash plumbing.Hash, keys []int, vals []int, statuses ...Sta
 }
 
 // Clone copies the file. It performs a deep copy of the tree;
-// depending on `clearStatuses` the original statuses are removed or not.
-// Any new `statuses` are appended.
-func (file *File) Clone(clearStatuses bool, statuses ...Status) *File {
-	clone := &File{Hash: file.Hash, tree: file.tree.Clone(), statuses: file.statuses}
+// depending on `clearStatuses` the original updaters are removed or not.
+// Any new `updaters` are appended.
+func (file *File) Clone(clearStatuses bool, updaters ...Updater) *File {
+	clone := &File{Hash: file.Hash, tree: file.tree.Clone(), updaters: file.updaters}
 	if clearStatuses {
-		clone.statuses = []Status{}
+		clone.updaters = []Updater{}
 	}
-	for _, status := range statuses {
-		clone.statuses = append(clone.statuses, status)
+	for _, updater := range updaters {
+		clone.updaters = append(clone.updaters, updater)
 	}
 	return clone
 }
@@ -299,15 +290,6 @@ func (file *File) Merge(day int, others... *File) bool {
 	return true
 }
 
-// Status returns the bound status object by the specified index.
-func (file *File) Status(index int) interface{} {
-	if index < 0 || index >= len(file.statuses) {
-		panic(fmt.Sprintf("status index %d is out of bounds [0, %d)",
-			index, len(file.statuses)))
-	}
-	return file.statuses[index].data
-}
-
 // Dump formats the underlying line interval tree into a string.
 // Useful for error messages, panic()-s and debugging.
 func (file *File) Dump() string {

+ 20 - 15
internal/burndown/file_test.go

@@ -9,14 +9,15 @@ import (
 	"gopkg.in/src-d/go-git.v4/plumbing"
 )
 
-func updateStatusFile(
-	status interface{}, _ int, previousTime int, delta int) {
-	status.(map[int]int64)[previousTime] += int64(delta)
+func updateStatusFile(status map[int]int64, _, previousTime, delta int) {
+	status[previousTime] += int64(delta)
 }
 
 func fixtureFile() (*File, map[int]int64) {
 	status := map[int]int64{}
-	file := NewFile(plumbing.ZeroHash, 0, 100, NewStatus(status, updateStatusFile))
+	file := NewFile(plumbing.ZeroHash, 0, 100, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	return file, status
 }
 
@@ -111,7 +112,9 @@ func TestCloneFileClearStatus(t *testing.T) {
 	file.Update(4, 20, 10, 0)
 	// 0 0 | 20 4 | 30 1 | 50 0 | 130 -1        [0]: 100, [1]: 20, [4]: 10
 	newStatus := map[int]int64{}
-	clone := file.Clone(true, NewStatus(newStatus, updateStatusFile))
+	clone := file.Clone(true, func(a, b, c int) {
+		updateStatusFile(newStatus, a, b, c)
+	})
 	clone.Update(5, 45, 0, 10)
 	// 0 0 | 20 4 | 30 1 | 45 0 | 120 -1        [0]: -5, [1]: -5
 	clone.Update(6, 45, 5, 0)
@@ -168,7 +171,9 @@ func TestInsertFile(t *testing.T) {
 
 func TestZeroInitializeFile(t *testing.T) {
 	status := map[int]int64{}
-	file := NewFile(plumbing.ZeroHash, 0, 0, NewStatus(status, updateStatusFile))
+	file := NewFile(plumbing.ZeroHash, 0, 0, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	assert.NotContains(t, status, 0)
 	dump := file.Dump()
 	// Output:
@@ -419,7 +424,9 @@ func TestBug3File(t *testing.T) {
 
 func TestBug4File(t *testing.T) {
 	status := map[int]int64{}
-	file := NewFile(plumbing.ZeroHash, 0, 10, NewStatus(status, updateStatusFile))
+	file := NewFile(plumbing.ZeroHash, 0, 10, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	file.Update(125, 0, 20, 9)
 	file.Update(125, 0, 20, 20)
 	file.Update(166, 12, 1, 1)
@@ -449,14 +456,18 @@ func TestBug5File(t *testing.T) {
 	status := map[int]int64{}
 	keys := []int{0, 2, 4, 7, 10}
 	vals := []int{24, 28, 24, 28, -1}
-	file := NewFileFromTree(plumbing.ZeroHash, keys, vals, NewStatus(status, updateStatusFile))
+	file := NewFileFromTree(plumbing.ZeroHash, keys, vals, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	file.Update(28, 0, 1, 3)
 	dump := file.Dump()
 	assert.Equal(t, "0 28\n2 24\n5 28\n8 -1\n", dump)
 
 	keys = []int{0, 1, 16, 18}
 	vals = []int{305, 0, 157, -1}
-	file = NewFileFromTree(plumbing.ZeroHash, keys, vals, NewStatus(status, updateStatusFile))
+	file = NewFileFromTree(plumbing.ZeroHash, keys, vals, func(a, b, c int) {
+		updateStatusFile(status, a, b, c)
+	})
 	file.Update(310, 0, 0, 2)
 	dump = file.Dump()
 	assert.Equal(t, "0 0\n14 157\n16 -1\n", dump)
@@ -484,12 +495,6 @@ func TestUpdatePanic(t *testing.T) {
 	assert.Contains(t, paniced, "invalid tree state")
 }
 
-func TestFileStatus(t *testing.T) {
-	f, _ := fixtureFile()
-	assert.Panics(t, func() { f.Status(1) })
-	assert.NotNil(t, f.Status(0))
-}
-
 func TestFileValidate(t *testing.T) {
 	keys := [...]int{0}
 	vals := [...]int{-1}

+ 157 - 183
leaves/burndown.go

@@ -49,21 +49,25 @@ type BurndownAnalysis struct {
 
 	// Repository points to the analysed Git repository struct from go-git.
 	repository *git.Repository
-	// globalStatus is the current daily alive number of lines; key is the number
-	// of days from the beginning of the history.
-	globalStatus map[int]int64
-	// globalHistory is the periodic snapshots of globalStatus.
-	globalHistory [][]int64
-	// fileHistories is the periodic snapshots of each file's status.
-	fileHistories map[string][][]int64
-	// peopleHistories is the periodic snapshots of each person's status.
-	peopleHistories [][][]int64
+	// globalHistory is the daily deltas of daily line counts.
+	// E.g. day 0: day 0 +50 lines
+	//      day 10: day 0 -10 lines; day 10 +20 lines
+	//      day 12: day 0 -5 lines; day 10 -3 lines; day 12 +10 lines
+	// map [0] [0] = 50
+	// map[10] [0] = -10
+	// map[10][10] = 20
+	// map[12] [0] = -5
+	// map[12][10] = -3
+	// map[12][12] = 10
+	globalHistory sparseHistory
+	// fileHistories is the daily deltas of each file's daily line counts.
+	fileHistories map[string]sparseHistory
+	// peopleHistories is the daily deltas of each person's daily line counts.
+	peopleHistories []sparseHistory
 	// files is the mapping <file path> -> *File.
 	files map[string]*burndown.File
 	// matrix is the mutual deletions and self insertions.
 	matrix []map[int]int64
-	// people is the people's individual time stats.
-	people []map[int]int64
 	// day is the most recent day index processed.
 	day int
 	// previousDay is the day from the previous sample period -
@@ -79,18 +83,18 @@ type BurndownResult struct {
 	// [number of samples][number of bands]
 	// The number of samples depends on Sampling: the less Sampling, the bigger the number.
 	// The number of bands depends on Granularity: the less Granularity, the bigger the number.
-	GlobalHistory [][]int64
+	GlobalHistory DenseHistory
 	// The key is the path inside the Git repository. The value's dimensions are the same as
 	// in GlobalHistory.
-	FileHistories map[string][][]int64
+	FileHistories map[string]DenseHistory
 	// [number of people][number of samples][number of bands]
-	PeopleHistories [][][]int64
+	PeopleHistories []DenseHistory
 	// [number of people][number of people + 2]
 	// The first element is the total number of lines added by the author.
 	// The second element is the number of removals by unidentified authors (outside reversedPeopleDict).
 	// The rest of the elements are equal the number of line removals by the corresponding
 	// authors in reversedPeopleDict: 2 -> 0, 3 -> 1, etc.
-	PeopleMatrix [][]int64
+	PeopleMatrix DenseHistory
 
 	// The following members are private.
 
@@ -123,6 +127,11 @@ const (
 	authorSelf = (1 << (32 - burndown.TreeMaxBinPower)) - 2
 )
 
+type sparseHistory = map[int]map[int]int64
+
+// DenseHistory is the matrix [number of samples][number of bands] -> number of lines.
+type DenseHistory = [][]int64
+
 // Name of this PipelineItem. Uniquely identifies the type, used for mapping keys, etc.
 func (analyser *BurndownAnalysis) Name() string {
 	return "Burndown"
@@ -225,18 +234,16 @@ func (analyser *BurndownAnalysis) Initialize(repository *git.Repository) {
 		analyser.Sampling = analyser.Granularity
 	}
 	analyser.repository = repository
-	analyser.globalStatus = map[int]int64{}
-	analyser.globalHistory = [][]int64{}
-	analyser.fileHistories = map[string][][]int64{}
-	analyser.peopleHistories = make([][][]int64, analyser.PeopleNumber)
+	analyser.globalHistory = sparseHistory{}
+	analyser.fileHistories = map[string]sparseHistory{}
+	analyser.peopleHistories = make([]sparseHistory, analyser.PeopleNumber)
 	analyser.files = map[string]*burndown.File{}
 	analyser.matrix = make([]map[int]int64, analyser.PeopleNumber)
-	analyser.people = make([]map[int]int64, analyser.PeopleNumber)
 	analyser.day = 0
 	analyser.previousDay = 0
 }
 
-// Consume runs this PipelineItem on the next commit data.
+// Consume runs this PipelineItem on the next commit's data.
 // `deps` contain all the results from upstream PipelineItem-s as requested by Requires().
 // Additionally, DependencyCommit is always present there and represents the analysed *object.Commit.
 // This function returns the mapping with analysis results. The keys must be the same as
@@ -309,19 +316,15 @@ func (analyser *BurndownAnalysis) Merge(branches []core.PipelineItem) {
 
 // Finalize returns the result of the analysis. Further Consume() calls are not expected.
 func (analyser *BurndownAnalysis) Finalize() interface{} {
-	gs, fss, pss := analyser.groupStatus()
-	analyser.updateHistories(1, gs, fss, pss)
-	for key, statuses := range analyser.fileHistories {
-		if len(statuses) == len(analyser.globalHistory) {
-			continue
-		}
-		padding := make([][]int64, len(analyser.globalHistory)-len(statuses))
-		for i := range padding {
-			padding[i] = make([]int64, len(analyser.globalStatus))
-		}
-		analyser.fileHistories[key] = append(padding, statuses...)
+	fileHistories := map[string]DenseHistory{}
+	for key, history := range analyser.fileHistories {
+		fileHistories[key] = analyser.groupSparseHistory(history)
+	}
+	peopleHistories := make([]DenseHistory, analyser.PeopleNumber)
+	for i, history := range analyser.peopleHistories {
+		peopleHistories[i] = analyser.groupSparseHistory(history)
 	}
-	peopleMatrix := make([][]int64, analyser.PeopleNumber)
+	peopleMatrix := make(DenseHistory, analyser.PeopleNumber)
 	for i, row := range analyser.matrix {
 		mrow := make([]int64, analyser.PeopleNumber+2)
 		peopleMatrix[i] = mrow
@@ -335,9 +338,9 @@ func (analyser *BurndownAnalysis) Finalize() interface{} {
 		}
 	}
 	return BurndownResult{
-		GlobalHistory:      analyser.globalHistory,
-		FileHistories:      analyser.fileHistories,
-		PeopleHistories:    analyser.peopleHistories,
+		GlobalHistory:      analyser.groupSparseHistory(analyser.globalHistory),
+		FileHistories:      fileHistories,
+		PeopleHistories:    peopleHistories,
 		PeopleMatrix:       peopleMatrix,
 		reversedPeopleDict: analyser.reversedPeopleDict,
 		sampling:           analyser.Sampling,
@@ -364,8 +367,8 @@ func (analyser *BurndownAnalysis) Deserialize(pbmessage []byte) (interface{}, er
 		return nil, err
 	}
 	result := BurndownResult{}
-	convertCSR := func(mat *pb.BurndownSparseMatrix) [][]int64 {
-		res := make([][]int64, mat.NumberOfRows)
+	convertCSR := func(mat *pb.BurndownSparseMatrix) DenseHistory {
+		res := make(DenseHistory, mat.NumberOfRows)
 		for i := 0; i < int(mat.NumberOfRows); i++ {
 			res[i] = make([]int64, mat.NumberOfColumns)
 			for j := 0; j < len(mat.Rows[i].Columns); j++ {
@@ -375,18 +378,18 @@ func (analyser *BurndownAnalysis) Deserialize(pbmessage []byte) (interface{}, er
 		return res
 	}
 	result.GlobalHistory = convertCSR(msg.Project)
-	result.FileHistories = map[string][][]int64{}
+	result.FileHistories = map[string]DenseHistory{}
 	for _, mat := range msg.Files {
 		result.FileHistories[mat.Name] = convertCSR(mat)
 	}
 	result.reversedPeopleDict = make([]string, len(msg.People))
-	result.PeopleHistories = make([][][]int64, len(msg.People))
+	result.PeopleHistories = make([]DenseHistory, len(msg.People))
 	for i, mat := range msg.People {
 		result.PeopleHistories[i] = convertCSR(mat)
 		result.reversedPeopleDict[i] = mat.Name
 	}
 	if msg.PeopleInteraction != nil {
-		result.PeopleMatrix = make([][]int64, msg.PeopleInteraction.NumberOfRows)
+		result.PeopleMatrix = make(DenseHistory, msg.PeopleInteraction.NumberOfRows)
 	}
 	for i := 0; i < len(result.PeopleMatrix); i++ {
 		result.PeopleMatrix[i] = make([]int64, msg.PeopleInteraction.NumberOfColumns)
@@ -431,12 +434,12 @@ func (analyser *BurndownAnalysis) MergeResults(
 		}()
 	}
 	if len(bar1.FileHistories) > 0 || len(bar2.FileHistories) > 0 {
-		merged.FileHistories = map[string][][]int64{}
+		merged.FileHistories = map[string]DenseHistory{}
 		historyMutex := sync.Mutex{}
 		for key, fh1 := range bar1.FileHistories {
 			if fh2, exists := bar2.FileHistories[key]; exists {
 				wg.Add(1)
-				go func(fh1, fh2 [][]int64, key string) {
+				go func(fh1, fh2 DenseHistory, key string) {
 					defer wg.Done()
 					historyMutex.Lock()
 					defer historyMutex.Unlock()
@@ -458,7 +461,7 @@ func (analyser *BurndownAnalysis) MergeResults(
 		}
 	}
 	if len(merged.reversedPeopleDict) > 0 {
-		merged.PeopleHistories = make([][][]int64, len(merged.reversedPeopleDict))
+		merged.PeopleHistories = make([]DenseHistory, len(merged.reversedPeopleDict))
 		for i, key := range merged.reversedPeopleDict {
 			ptrs := people[key]
 			if ptrs[1] < 0 {
@@ -473,7 +476,7 @@ func (analyser *BurndownAnalysis) MergeResults(
 				wg.Add(1)
 				go func(i int) {
 					defer wg.Done()
-					var m1, m2 [][]int64
+					var m1, m2 DenseHistory
 					if len(bar1.PeopleHistories) > 0 {
 						m1 = bar1.PeopleHistories[ptrs[1]]
 					}
@@ -505,7 +508,7 @@ func (analyser *BurndownAnalysis) MergeResults(
 						merged.PeopleMatrix, make([]int64, len(merged.reversedPeopleDict)+2))
 				}
 			} else {
-				merged.PeopleMatrix = make([][]int64, len(merged.reversedPeopleDict))
+				merged.PeopleMatrix = make(DenseHistory, len(merged.reversedPeopleDict))
 				for i := range merged.PeopleMatrix {
 					merged.PeopleMatrix[i] = make([]int64, len(merged.reversedPeopleDict)+2)
 				}
@@ -534,8 +537,8 @@ func (analyser *BurndownAnalysis) MergeResults(
 // mergeMatrices takes two [number of samples][number of bands] matrices,
 // resamples them to days so that they become square, sums and resamples back to the
 // least of (sampling1, sampling2) and (granularity1, granularity2).
-func mergeMatrices(m1, m2 [][]int64, granularity1, sampling1, granularity2, sampling2 int,
-	c1, c2 *core.CommonAnalysisResult) [][]int64 {
+func mergeMatrices(m1, m2 DenseHistory, granularity1, sampling1, granularity2, sampling2 int,
+	c1, c2 *core.CommonAnalysisResult) DenseHistory {
 	commonMerged := *c1
 	commonMerged.Merge(c2)
 
@@ -565,8 +568,8 @@ func mergeMatrices(m1, m2 [][]int64, granularity1, sampling1, granularity2, samp
 			int(c2.BeginTime-commonMerged.BeginTime)/(3600*24))
 	}
 
-	// convert daily to [][]in(t64
-	result := make([][]int64, (size+sampling-1)/sampling)
+	// convert daily to [][]int64
+	result := make(DenseHistory, (size+sampling-1)/sampling)
 	for i := range result {
 		result[i] = make([]int64, (size+granularity-1)/granularity)
 		sampledIndex := i * sampling
@@ -590,7 +593,7 @@ func mergeMatrices(m1, m2 [][]int64, granularity1, sampling1, granularity2, samp
 // Rows: *at least* len(matrix) * sampling + offset
 // Columns: *at least* len(matrix[...]) * granularity + offset
 // `matrix` can be sparse, so that the last columns which are equal to 0 are truncated.
-func addBurndownMatrix(matrix [][]int64, granularity, sampling int, daily [][]float32, offset int) {
+func addBurndownMatrix(matrix DenseHistory, granularity, sampling int, daily [][]float32, offset int) {
 	// Determine the maximum number of bands; the actual one may be larger but we do not care
 	maxCols := 0
 	for _, row := range matrix {
@@ -808,7 +811,7 @@ func (analyser *BurndownAnalysis) serializeBinary(result *BurndownResult, writer
 	return nil
 }
 
-func sortedKeys(m map[string][][]int64) []string {
+func sortedKeys(m map[string]DenseHistory) []string {
 	keys := make([]string, 0, len(m))
 	for k := range m {
 		keys = append(keys, k)
@@ -846,54 +849,70 @@ func (analyser *BurndownAnalysis) unpackPersonWithDay(value int) (int, int) {
 }
 
 func (analyser *BurndownAnalysis) onNewDay() {
-	day := analyser.day
-	sampling := analyser.Sampling
-	delta := (day / sampling) - (analyser.previousDay / sampling)
-	if delta > 0 {
-		analyser.previousDay = day
-		gs, fss, pss := analyser.groupStatus()
-		analyser.updateHistories(delta, gs, fss, pss)
+	if analyser.day > analyser.previousDay {
+		analyser.previousDay = analyser.day
 	}
 }
 
-func (analyser *BurndownAnalysis) updateStatus(
-	status interface{}, _ int, previousValue int, delta int) {
+func (analyser *BurndownAnalysis) updateGlobal(currentTime, previousTime, delta int) {
+	_, currentDay := analyser.unpackPersonWithDay(currentTime)
+	_, previousDay := analyser.unpackPersonWithDay(previousTime)
+	currentHistory := analyser.globalHistory[currentDay]
+	if currentHistory == nil {
+		currentHistory = map[int]int64{}
+		analyser.globalHistory[currentDay] = currentHistory
+	}
+	currentHistory[previousDay] += int64(delta)
+}
+
+// updateFile is bound to the specific `history` in the closure.
+func (analyser *BurndownAnalysis) updateFile(
+	history sparseHistory, currentTime, previousTime, delta int) {
+		
+	_, currentDay := analyser.unpackPersonWithDay(currentTime)
+	_, previousDay := analyser.unpackPersonWithDay(previousTime)
 
-	_, previousTime := analyser.unpackPersonWithDay(previousValue)
-	status.(map[int]int64)[previousTime] += int64(delta)
+	currentHistory := history[currentDay]
+	if currentHistory == nil {
+		currentHistory = map[int]int64{}
+		history[currentDay] = currentHistory
+	}
+	currentHistory[previousDay] += int64(delta)
 }
 
-func (analyser *BurndownAnalysis) updatePeople(
-	peopleUncasted interface{}, _ int, previousValue int, delta int) {
-	previousAuthor, previousTime := analyser.unpackPersonWithDay(previousValue)
+func (analyser *BurndownAnalysis) updateAuthor(currentTime, previousTime, delta int) {
+	previousAuthor, previousDay := analyser.unpackPersonWithDay(previousTime)
 	if previousAuthor == identity.AuthorMissing {
 		return
 	}
-	people := peopleUncasted.([]map[int]int64)
-	stats := people[previousAuthor]
-	if stats == nil {
-		stats = map[int]int64{}
-		people[previousAuthor] = stats
+	_, currentDay := analyser.unpackPersonWithDay(currentTime)
+	history := analyser.peopleHistories[previousAuthor]
+	if history == nil {
+		history = sparseHistory{}
+		analyser.peopleHistories[previousAuthor] = history
 	}
-	stats[previousTime] += int64(delta)
+	currentHistory := history[currentDay]
+	if currentHistory == nil {
+		currentHistory = map[int]int64{}
+		history[currentDay] = currentHistory
+	}
+	currentHistory[previousDay] += int64(delta)
 }
 
-func (analyser *BurndownAnalysis) updateMatrix(
-	matrixUncasted interface{}, currentTime int, previousTime int, delta int) {
-
-	matrix := matrixUncasted.([]map[int]int64)
+func (analyser *BurndownAnalysis) updateMatrix(currentTime, previousTime, delta int) {
 	newAuthor, _ := analyser.unpackPersonWithDay(currentTime)
 	oldAuthor, _ := analyser.unpackPersonWithDay(previousTime)
+
 	if oldAuthor == identity.AuthorMissing {
 		return
 	}
 	if newAuthor == oldAuthor && delta > 0 {
 		newAuthor = authorSelf
 	}
-	row := matrix[oldAuthor]
+	row := analyser.matrix[oldAuthor]
 	if row == nil {
 		row = map[int]int64{}
-		matrix[oldAuthor] = row
+		analyser.matrix[oldAuthor] = row
 	}
 	cell, exists := row[newAuthor]
 	if !exists {
@@ -904,19 +923,25 @@ func (analyser *BurndownAnalysis) updateMatrix(
 }
 
 func (analyser *BurndownAnalysis) newFile(
-	hash plumbing.Hash, author int, day int, size int, global map[int]int64,
-	people []map[int]int64, matrix []map[int]int64) *burndown.File {
-	statuses := make([]burndown.Status, 1)
-	statuses[0] = burndown.NewStatus(global, analyser.updateStatus)
+	hash plumbing.Hash, name string, author int, day int, size int) (*burndown.File, error) {
+	statuses := make([]burndown.Updater, 1)
+	statuses[0] = analyser.updateGlobal
 	if analyser.TrackFiles {
-		statuses = append(statuses, burndown.NewStatus(map[int]int64{}, analyser.updateStatus))
+		if _, exists := analyser.fileHistories[name]; exists {
+			return nil, fmt.Errorf("file %s already exists", name)
+		}
+		history := sparseHistory{}
+		analyser.fileHistories[name] = history
+		statuses = append(statuses, func(currentTime, previousTime, delta int) {
+			analyser.updateFile(history, currentTime, previousTime, delta)
+		})
 	}
 	if analyser.PeopleNumber > 0 {
-		statuses = append(statuses, burndown.NewStatus(people, analyser.updatePeople))
-		statuses = append(statuses, burndown.NewStatus(matrix, analyser.updateMatrix))
+		statuses = append(statuses, analyser.updateAuthor)
+		statuses = append(statuses, analyser.updateMatrix)
 		day = analyser.packPersonWithDay(author, day)
 	}
-	return burndown.NewFile(hash, day, size, statuses...)
+	return burndown.NewFile(hash, day, size, statuses...), nil
 }
 
 func (analyser *BurndownAnalysis) handleInsertion(
@@ -934,11 +959,9 @@ func (analyser *BurndownAnalysis) handleInsertion(
 	if exists {
 		return fmt.Errorf("file %s already exists", name)
 	}
-	file = analyser.newFile(
-		blob.Hash, author, analyser.day, lines,
-		analyser.globalStatus, analyser.people, analyser.matrix)
+	file, err = analyser.newFile(blob.Hash, name, author, analyser.day, lines)
 	analyser.files[name] = file
-	return nil
+	return err
 }
 
 func (analyser *BurndownAnalysis) handleDeletion(
@@ -957,6 +980,7 @@ func (analyser *BurndownAnalysis) handleDeletion(
 	file.Update(analyser.packPersonWithDay(author, analyser.day), 0, 0, lines)
 	file.Hash = plumbing.ZeroHash
 	delete(analyser.files, name)
+	delete(analyser.fileHistories, name)
 	return nil
 }
 
@@ -1066,113 +1090,63 @@ func (analyser *BurndownAnalysis) handleModification(
 }
 
 func (analyser *BurndownAnalysis) handleRename(from, to string) error {
+	if from == to {
+		return nil
+	}
 	file, exists := analyser.files[from]
 	if !exists {
 		return fmt.Errorf("file %s does not exist", from)
 	}
 	analyser.files[to] = file
 	delete(analyser.files, from)
-	return nil
-}
 
-func (analyser *BurndownAnalysis) groupStatus() ([]int64, map[string][]int64, [][]int64) {
-	granularity := analyser.Granularity
-	if granularity == 0 {
-		granularity = 1
-	}
-	day := analyser.day
-	day++
-	adjust := 0
-	if day%granularity != 0 {
-		adjust = 1
-	}
-	global := make([]int64, day/granularity+adjust)
-	var group int64
-	for i := 0; i < day; i++ {
-		group += analyser.globalStatus[i]
-		if (i % granularity) == (granularity - 1) {
-			global[i/granularity] = group
-			group = 0
-		}
-	}
-	if day%granularity != 0 {
-		global[len(global)-1] = group
-	}
-	locals := make(map[string][]int64)
-	if analyser.TrackFiles {
-		for key, file := range analyser.files {
-			status := make([]int64, day/granularity+adjust)
-			var group int64
-			for i := 0; i < day; i++ {
-				group += file.Status(1).(map[int]int64)[i]
-				if (i % granularity) == (granularity - 1) {
-					status[i/granularity] = group
-					group = 0
-				}
-			}
-			if day%granularity != 0 {
-				status[len(status)-1] = group
-			}
-			locals[key] = status
-		}
-	}
-	peoples := make([][]int64, len(analyser.people))
-	for key, person := range analyser.people {
-		status := make([]int64, day/granularity+adjust)
-		var group int64
-		for i := 0; i < day; i++ {
-			group += person[i]
-			if (i % granularity) == (granularity - 1) {
-				status[i/granularity] = group
-				group = 0
-			}
-		}
-		if day%granularity != 0 {
-			status[len(status)-1] = group
-		}
-		peoples[key] = status
+	history, exists := analyser.fileHistories[from]
+	if !exists {
+		return fmt.Errorf("file %s does not exist", from)
 	}
-	return global, locals, peoples
+	analyser.fileHistories[to] = history
+	delete(analyser.fileHistories, from)
+	return nil
 }
 
-func (analyser *BurndownAnalysis) updateHistories(
-	delta int, globalStatus []int64, fileStatuses map[string][]int64, peopleStatuses [][]int64) {
-	for i := 0; i < delta; i++ {
-		analyser.globalHistory = append(analyser.globalHistory, globalStatus)
-	}
-	toDelete := make([]string, 0)
-	for key, fh := range analyser.fileHistories {
-		ls, exists := fileStatuses[key]
-		if !exists {
-			toDelete = append(toDelete, key)
-		} else {
-			for i := 0; i < delta; i++ {
-				fh = append(fh, ls)
+func (analyser *BurndownAnalysis) groupSparseHistory(history sparseHistory) DenseHistory {
+	var days []int
+	for day := range history {
+		days = append(days, day)
+	}
+	sort.Ints(days)
+	// [y][x]
+	// y - sampling
+	// x - granularity
+	maxDay := days[len(days)-1]
+	samples := maxDay / analyser.Sampling
+	if (samples + 1) * analyser.Sampling - 1 != maxDay {
+		samples++
+	}
+	bands := maxDay / analyser.Granularity
+	if (bands + 1) * analyser.Granularity - 1 != maxDay {
+		bands++
+	}
+	result := make(DenseHistory, samples)
+	for i := 0; i < bands; i++ {
+		result[i] = make([]int64, bands)
+	}
+	prevsi := 0
+	for _, day := range days {
+		si := day / analyser.Sampling
+		if si > prevsi {
+			state := result[prevsi]
+			for i := prevsi + 1; i <= si; i++ {
+				copy(result[i], state)
 			}
-			analyser.fileHistories[key] = fh
-		}
-	}
-	for _, key := range toDelete {
-		delete(analyser.fileHistories, key)
-	}
-	for key, ls := range fileStatuses {
-		fh, exists := analyser.fileHistories[key]
-		if exists {
-			continue
+			prevsi = si
 		}
-		for i := 0; i < delta; i++ {
-			fh = append(fh, ls)
+		sample := result[si]
+		for bday, value := range history[day] {
+			sample[bday / analyser.Granularity] += value
 		}
-		analyser.fileHistories[key] = fh
-	}
-
-	for key, ph := range analyser.peopleHistories {
-		ls := peopleStatuses[key]
-		for i := 0; i < delta; i++ {
-			ph = append(ph, ls)
-		}
-		analyser.peopleHistories[key] = ph
 	}
+	return result
 }
 
 func init() {

+ 35 - 37
leaves/burndown_test.go

@@ -18,12 +18,12 @@ import (
 	items "gopkg.in/src-d/hercules.v4/internal/plumbing"
 	"gopkg.in/src-d/hercules.v4/internal/plumbing/identity"
 	"gopkg.in/src-d/hercules.v4/internal/test"
-)
+	)
 
 func TestBurndownMeta(t *testing.T) {
 	burndown := BurndownAnalysis{}
 	assert.Equal(t, burndown.Name(), "Burndown")
-	assert.Equal(t, len(burndown.Provides()), 0)
+	assert.Len(t, burndown.Provides(), 0)
 	required := [...]string{
 		items.DependencyFileDiff, items.DependencyTreeChanges, items.DependencyBlobCache,
 		items.DependencyDay, identity.DependencyAuthor}
@@ -184,16 +184,15 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	assert.Nil(t, result)
 	assert.Nil(t, err)
 	assert.Equal(t, burndown.previousDay, 0)
-	assert.Equal(t, len(burndown.files), 3)
+	assert.Len(t, burndown.files, 3)
 	assert.Equal(t, burndown.files["cmd/hercules/main.go"].Len(), 207)
 	assert.Equal(t, burndown.files["analyser.go"].Len(), 926)
 	assert.Equal(t, burndown.files[".travis.yml"].Len(), 12)
-	assert.Equal(t, len(burndown.people), 2)
-	assert.Equal(t, burndown.people[0][0], int64(12+207+926))
-	assert.Equal(t, len(burndown.globalStatus), 1)
-	assert.Equal(t, burndown.globalStatus[0], int64(12+207+926))
-	assert.Equal(t, len(burndown.globalHistory), 0)
-	assert.Equal(t, len(burndown.fileHistories), 0)
+	assert.Len(t, burndown.peopleHistories, 2)
+	assert.Equal(t, burndown.peopleHistories[0][0][0], int64(12+207+926))
+	assert.Len(t, burndown.globalHistory, 1)
+	assert.Equal(t, burndown.globalHistory[0][0], int64(12+207+926))
+	assert.Len(t, burndown.fileHistories, 3)
 	burndown2 := BurndownAnalysis{
 		Granularity: 30,
 		Sampling:    0,
@@ -201,9 +200,8 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	burndown2.Initialize(test.Repository)
 	_, err = burndown2.Consume(deps)
 	assert.Nil(t, err)
-	assert.Equal(t, len(burndown2.people), 0)
-	assert.Equal(t, len(burndown2.peopleHistories), 0)
-	assert.Equal(t, len(burndown2.fileHistories), 0)
+	assert.Len(t, burndown2.peopleHistories, 0)
+	assert.Len(t, burndown2.fileHistories, 0)
 
 	// stage 2
 	// 2b1ed978194a94edeabbca6de7ff3b5771d4d665
@@ -281,16 +279,15 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	assert.Nil(t, result)
 	assert.Nil(t, err)
 	assert.Equal(t, burndown.previousDay, 30)
-	assert.Equal(t, len(burndown.files), 2)
+	assert.Len(t, burndown.files, 2)
 	assert.Equal(t, burndown.files["cmd/hercules/main.go"].Len(), 290)
 	assert.Equal(t, burndown.files["burndown.go"].Len(), 543)
-	assert.Equal(t, len(burndown.people), 2)
-	assert.Equal(t, len(burndown.globalStatus), 2)
-	assert.Equal(t, burndown.globalStatus[0], int64(464))
-	assert.Equal(t, burndown.globalStatus[1], int64(0))
-	assert.Equal(t, len(burndown.globalHistory), 1)
-	assert.Equal(t, len(burndown.globalHistory[0]), 2)
-	assert.Equal(t, len(burndown.fileHistories), 3)
+	assert.Len(t, burndown.peopleHistories, 2)
+	assert.Len(t, burndown.globalHistory, 2)
+	assert.Equal(t, burndown.globalHistory[0][0], int64(1145))
+	assert.Equal(t, burndown.globalHistory[30][0], int64(-681))
+	assert.Equal(t, burndown.globalHistory[30][30], int64(369))
+	assert.Len(t, burndown.fileHistories, 2)
 	out := burndown.Finalize().(BurndownResult)
 	/*
 		GlobalHistory   [][]int64
@@ -298,23 +295,23 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 		PeopleHistories [][][]int64
 		PeopleMatrix    [][]int64
 	*/
-	assert.Equal(t, len(out.GlobalHistory), 2)
+	assert.Len(t, out.GlobalHistory, 2)
 	for i := 0; i < 2; i++ {
-		assert.Equal(t, len(out.GlobalHistory[i]), 2)
+		assert.Len(t, out.GlobalHistory[i], 2)
 	}
-	assert.Equal(t, len(out.GlobalHistory), 2)
+	assert.Len(t, out.GlobalHistory, 2)
 	assert.Equal(t, out.GlobalHistory[0][0], int64(1145))
 	assert.Equal(t, out.GlobalHistory[0][1], int64(0))
 	assert.Equal(t, out.GlobalHistory[1][0], int64(464))
 	assert.Equal(t, out.GlobalHistory[1][1], int64(369))
-	assert.Equal(t, len(out.FileHistories), 2)
-	assert.Equal(t, len(out.FileHistories["cmd/hercules/main.go"]), 2)
-	assert.Equal(t, len(out.FileHistories["burndown.go"]), 2)
-	assert.Equal(t, len(out.FileHistories["cmd/hercules/main.go"][0]), 2)
-	assert.Equal(t, len(out.FileHistories["burndown.go"][0]), 2)
-	assert.Equal(t, len(out.PeopleMatrix), 2)
-	assert.Equal(t, len(out.PeopleMatrix[0]), 4)
-	assert.Equal(t, len(out.PeopleMatrix[1]), 4)
+	assert.Len(t, out.FileHistories, 2)
+	assert.Len(t, out.FileHistories["cmd/hercules/main.go"], 2)
+	assert.Len(t, out.FileHistories["burndown.go"], 2)
+	assert.Len(t, out.FileHistories["cmd/hercules/main.go"][0], 2)
+	assert.Len(t, out.FileHistories["burndown.go"][0], 2)
+	assert.Len(t, out.PeopleMatrix, 2)
+	assert.Len(t, out.PeopleMatrix[0], 4)
+	assert.Len(t, out.PeopleMatrix[1], 4)
 	assert.Equal(t, out.PeopleMatrix[0][0], int64(1145))
 	assert.Equal(t, out.PeopleMatrix[0][1], int64(0))
 	assert.Equal(t, out.PeopleMatrix[0][2], int64(0))
@@ -323,11 +320,11 @@ func TestBurndownConsumeFinalize(t *testing.T) {
 	assert.Equal(t, out.PeopleMatrix[1][1], int64(0))
 	assert.Equal(t, out.PeopleMatrix[1][2], int64(0))
 	assert.Equal(t, out.PeopleMatrix[1][3], int64(0))
-	assert.Equal(t, len(out.PeopleHistories), 2)
+	assert.Len(t, out.PeopleHistories, 2)
 	for i := 0; i < 2; i++ {
-		assert.Equal(t, len(out.PeopleHistories[i]), 2)
-		assert.Equal(t, len(out.PeopleHistories[i][0]), 2)
-		assert.Equal(t, len(out.PeopleHistories[i][1]), 2)
+		assert.Len(t, out.PeopleHistories[i], 2)
+		assert.Len(t, out.PeopleHistories[i][0], 2)
+		assert.Len(t, out.PeopleHistories[i][1], 2)
 	}
 }
 
@@ -488,7 +485,7 @@ func TestBurndownSerialize(t *testing.T) {
      464  369
   files:
     "burndown.go": |-
-      0     0
+      926   0
       293 250
     "cmd/hercules/main.go": |-
       207   0
@@ -526,7 +523,8 @@ func TestBurndownSerialize(t *testing.T) {
 	assert.Equal(t, msg.Files[0].Name, "burndown.go")
 	assert.Equal(t, msg.Files[1].Name, "cmd/hercules/main.go")
 	assert.Len(t, msg.Files[0].Rows, 2)
-	assert.Len(t, msg.Files[0].Rows[0].Columns, 0)
+	assert.Len(t, msg.Files[0].Rows[0].Columns, 1)
+	assert.Equal(t, msg.Files[0].Rows[0].Columns[0], uint32(926))
 	assert.Len(t, msg.Files[0].Rows[1].Columns, 2)
 	assert.Equal(t, msg.Files[0].Rows[1].Columns[0], uint32(293))
 	assert.Equal(t, msg.Files[0].Rows[1].Columns[1], uint32(250))