| // Copyright 2022 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| package cov |
| |
| import ( |
| "log" |
| "sort" |
| "sync" |
| ) |
| |
| // Optimize optimizes the Tree by de-duplicating common spans into a tree of SpanGroups. |
| // |
| // Breaking down tests into group hierarchies provide a natural way to structure |
| // coverage data, as tests of the same suite, file or test are likely to have |
| // similar coverage spans. |
| // |
| // For each source file in the codebase, we create a tree of SpanGroups, where the |
| // leaves are the test cases. |
| // |
| // For example, given the following Paths: |
| // |
| // a.b.d.h |
| // a.b.d.i.n |
| // a.b.d.i.o |
| // a.b.e.j |
| // a.b.e.k.p |
| // a.b.e.k.q |
| // a.c.f |
| // a.c.g.l.r |
| // a.c.g.m |
| // |
| // We would construct the following tree: |
| // |
| // a |
| // ╭──────┴──────╮ |
| // b c |
| // ╭───┴───╮ ╭───┴───╮ |
| // d e f g |
| // ╭─┴─╮ ╭─┴─╮ ╭─┴─╮ |
| // h i j k l m |
| // ╭┴╮ ╭┴╮ │ |
| // n o p q r |
| // |
| // Each leaf node in this tree (`h`, `n`, `o`, `j`, `p`, `q`, `f`, `r`, `m`) |
| // represent a test case, and non-leaf nodes (`a`, `b`, `c`, `d`, `e`, `g`, `i`, |
| // `k`, `l`) are suite, file or tests. |
| // |
| // To begin, we create a test tree structure, and associate the full list of test |
| // coverage spans with every leaf node (test case) in this tree. |
| // |
| // This data structure hasn't given us any compression benefits yet, but we can |
| // now do a few tricks to dramatically reduce number of spans needed to describe |
| // the graph: |
| // |
| // ~ Optimization 1: Common span promotion ~ |
| // |
| // The first compression scheme is to promote common spans up the tree when they |
| // are common for all children. This will reduce the number of spans needed to be |
| // encoded in the final file. |
| // |
| // For example, if the test group `a` has 4 children that all share the same span |
| // `X`: |
| // |
| // a |
| // ╭───┬─┴─┬───╮ |
| // b c d e |
| // [X,Y] [X] [X] [X,Z] |
| // |
| // Then span `X` can be promoted up to `a`: |
| // |
| // [X] |
| // a |
| // ╭───┬─┴─┬───╮ |
| // b c d e |
| // [Y] [] [] [Z] |
| // |
| // ~ Optimization 2: Span XOR promotion ~ |
| // |
| // This idea can be extended further, by not requiring all the children to share |
| // the same span before promotion. If *most* child nodes share the same span, we |
| // can still promote the span, but this time we *remove* the span from the |
| // children *if they had it*, and *add* the span to children *if they didn't |
| // have it*. |
| // |
| // For example, if the test group `a` has 4 children with 3 that share the span |
| // `X`: |
| // |
| // a |
| // ╭───┬─┴─┬───╮ |
| // b c d e |
| // [X,Y] [X] [] [X,Z] |
| // |
| // Then span `X` can be promoted up to `a` by flipping the presence of `X` on the |
| // child nodes: |
| // |
| // [X] |
| // a |
| // ╭───┬─┴─┬───╮ |
| // b c d e |
| // [Y] [] [X] [Z] |
| // |
| // This process repeats up the tree. |
| // |
| // With this optimization applied, we now need to traverse the tree from root to |
| // leaf in order to know whether a given span is in use for the leaf node (test case): |
| // |
| // * If the span is encountered an *odd* number of times during traversal, then |
| // the span is *covered*. |
| // * If the span is encountered an *even* number of times during traversal, then |
| // the span is *not covered*. |
| // |
| // See tools/src/cov/coverage_test.go for more examples of this optimization. |
| // |
| // ~ Optimization 3: Common span grouping ~ |
| // |
| // With real world data, we encounter groups of spans that are commonly found |
| // together. To further reduce coverage data, the whole graph is scanned for common |
| // span patterns, and are indexed by each tree node. |
| // The XOR'ing of spans as described above is performed as if the spans were not |
| // grouped. |
| // |
| // ~ Optimization 4: Lookup tables ~ |
| // |
| // All spans, span-groups and strings are stored in de-duplicated tables, and are |
| // indexed wherever possible. |
| func (t *Tree) Optimize() { |
| log.Printf("Optimizing coverage tree...") |
| |
| // Start by gathering all of the unique spansets |
| wg := sync.WaitGroup{} |
| wg.Add(len(t.files)) |
| for _, file := range t.files { |
| file := file |
| go func() { |
| defer wg.Done() |
| o := optimizer{} |
| for idx, tc := range file.tcm { |
| o.invertForCommon(tc, &t.testRoot.children[idx]) |
| } |
| o.createGroups(file) |
| }() |
| } |
| wg.Wait() |
| } |
| |
| type optimizer struct{} |
| |
| // createGroups looks for common SpanSets, and creates indexable span groups |
| // which are then used instead. |
| func (o *optimizer) createGroups(f *treeFile) { |
| const minSpansInGroup = 2 |
| |
| type spansetKey string |
| spansetMap := map[spansetKey]SpanSet{} |
| |
| f.tcm.traverse(func(tc *TestCoverage) { |
| if len(tc.Spans) >= minSpansInGroup { |
| key := spansetKey(tc.Spans.String()) |
| if _, ok := spansetMap[key]; !ok { |
| spansetMap[key] = tc.Spans |
| } |
| } |
| }) |
| |
| if len(spansetMap) == 0 { |
| return |
| } |
| |
| type spansetInfo struct { |
| key spansetKey |
| set SpanSet // fully expanded set |
| grp SpanGroup |
| id SpanGroupID |
| } |
| spansets := make([]*spansetInfo, 0, len(spansetMap)) |
| for key, set := range spansetMap { |
| spansets = append(spansets, &spansetInfo{ |
| key: key, |
| set: set, |
| grp: SpanGroup{Spans: set}, |
| }) |
| } |
| |
| // Sort by number of spans in each sets starting with the largest. |
| sort.Slice(spansets, func(i, j int) bool { |
| a, b := spansets[i].set, spansets[j].set |
| switch { |
| case len(a) > len(b): |
| return true |
| case len(a) < len(b): |
| return false |
| } |
| return a.List().Compare(b.List()) == -1 // Just to keep output stable |
| }) |
| |
| // Assign IDs now that we have stable order. |
| for i := range spansets { |
| spansets[i].id = SpanGroupID(i) |
| } |
| |
| // Loop over the spanGroups starting from the largest, and try to fold them |
| // into the larger sets. |
| // This is O(n^2) complexity. |
| nextSpan: |
| for i, a := range spansets[:len(spansets)-1] { |
| for _, b := range spansets[i+1:] { |
| if len(a.set) > len(b.set) && a.set.containsAll(b.set) { |
| extend := b.id // Do not take address of iterator! |
| a.grp.Spans = a.set.removeAll(b.set) |
| a.grp.Extend = &extend |
| continue nextSpan |
| } |
| } |
| } |
| |
| // Rebuild a map of spansetKey to SpanGroup |
| spangroupMap := make(map[spansetKey]*spansetInfo, len(spansets)) |
| for _, s := range spansets { |
| spangroupMap[s.key] = s |
| } |
| |
| // Store the groups in the tree |
| f.spangroups = make(map[SpanGroupID]SpanGroup, len(spansets)) |
| for _, s := range spansets { |
| f.spangroups[s.id] = s.grp |
| } |
| |
| // Update all the uses. |
| f.tcm.traverse(func(tc *TestCoverage) { |
| key := spansetKey(tc.Spans.String()) |
| if g, ok := spangroupMap[key]; ok { |
| tc.Spans = nil |
| tc.Group = &g.id |
| } |
| }) |
| } |
| |
| // invertCommon looks for tree nodes with the majority of the child nodes with |
| // the same spans. This span is promoted up to the parent, and the children |
| // have the span inverted. |
| func (o *optimizer) invertForCommon(tc *TestCoverage, t *Test) { |
| wg := sync.WaitGroup{} |
| wg.Add(len(tc.Children)) |
| for id, child := range tc.Children { |
| id, child := id, child |
| go func() { |
| defer wg.Done() |
| o.invertForCommon(child, &t.children[id]) |
| }() |
| } |
| wg.Wait() |
| |
| counts := map[SpanID]int{} |
| for _, child := range tc.Children { |
| for span := range child.Spans { |
| counts[span] = counts[span] + 1 |
| } |
| } |
| |
| for span, count := range counts { |
| if count > len(t.children)/2 { |
| tc.Spans = tc.Spans.invert(span) |
| for _, idx := range t.indices { |
| child := tc.Children.index(idx) |
| child.Spans = child.Spans.invert(span) |
| if child.deletable() { |
| delete(tc.Children, idx) |
| } |
| } |
| } |
| } |
| } |