tools: Add src/cts/results.List.MinimalVariantTags

MinimalVariantTags accepts a list of tag-sets (e.g GPU tags, OS tags, etc),
and returns an optimized list of variants, folding together variants that
have identical result query-to-status mappings, and removing redundant tags.

Bug: dawn:1342
Change-Id: I759c82e9a0631a9d321d376656e5a2dbbf5f5507
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/87643
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
This commit is contained in:
Ben Clayton 2022-04-29 08:13:19 +00:00 committed by Dawn LUCI CQ
parent cede544df3
commit 2c1154c36f
6 changed files with 316 additions and 6 deletions

View File

@ -71,7 +71,7 @@ func (s Set[T]) Contains(item T) bool {
return found
}
// Contains returns true if the set contains all the items in o
// ContainsAll returns true if the set contains all the items in o
func (s Set[T]) ContainsAll(o Set[T]) bool {
for item := range o {
if !s.Contains(item) {
@ -81,6 +81,16 @@ func (s Set[T]) ContainsAll(o Set[T]) bool {
return true
}
// ContainsAny returns true if the set contains any of the items in o
func (s Set[T]) ContainsAny(o Set[T]) bool {
for item := range o {
if s.Contains(item) {
return true
}
}
return false
}
// Intersection returns true if the set contains all the items in o
func (s Set[T]) Intersection(o Set[T]) Set[T] {
out := NewSet[T]()

View File

@ -123,6 +123,39 @@ func TestSetContainsAll(t *testing.T) {
expectEq(t, `s.ContainsAll("c", "a", "b")`, s.ContainsAll(S("c", "a", "b")), true)
}
func TestSetContainsAny(t *testing.T) {
S := container.NewSet[string]
s := container.NewSet[string]()
s.Add("c")
expectEq(t, `s.ContainsAny("a")`, s.ContainsAny(S("a")), false)
expectEq(t, `s.ContainsAny("b")`, s.ContainsAny(S("b")), false)
expectEq(t, `s.ContainsAny("c")`, s.ContainsAny(S("c")), true)
expectEq(t, `s.ContainsAny("a", "b")`, s.ContainsAny(S("a", "b")), false)
expectEq(t, `s.ContainsAny("b", "c")`, s.ContainsAny(S("b", "c")), true)
expectEq(t, `s.ContainsAny("c", "a")`, s.ContainsAny(S("c", "a")), true)
expectEq(t, `s.ContainsAny("c", "a", "b")`, s.ContainsAny(S("c", "a", "b")), true)
s.Add("a")
expectEq(t, `s.ContainsAny("a")`, s.ContainsAny(S("a")), true)
expectEq(t, `s.ContainsAny("b")`, s.ContainsAny(S("b")), false)
expectEq(t, `s.ContainsAny("c")`, s.ContainsAny(S("c")), true)
expectEq(t, `s.ContainsAny("a", "b")`, s.ContainsAny(S("a", "b")), true)
expectEq(t, `s.ContainsAny("b", "c")`, s.ContainsAny(S("b", "c")), true)
expectEq(t, `s.ContainsAny("c", "a")`, s.ContainsAny(S("c", "a")), true)
expectEq(t, `s.ContainsAny("c", "a", "b")`, s.ContainsAny(S("c", "a", "b")), true)
s.Remove("c")
s.Add("b")
expectEq(t, `s.ContainsAny("a")`, s.ContainsAny(S("a")), true)
expectEq(t, `s.ContainsAny("b")`, s.ContainsAny(S("b")), true)
expectEq(t, `s.ContainsAny("c")`, s.ContainsAny(S("c")), false)
expectEq(t, `s.ContainsAny("a", "b")`, s.ContainsAny(S("a", "b")), true)
expectEq(t, `s.ContainsAny("b", "c")`, s.ContainsAny(S("b", "c")), true)
expectEq(t, `s.ContainsAny("c", "a")`, s.ContainsAny(S("c", "a")), true)
expectEq(t, `s.ContainsAny("c", "a", "b")`, s.ContainsAny(S("c", "a", "b")), true)
}
func TestSetIntersection(t *testing.T) {
a := container.NewSet(1, 3, 4, 6)
b := container.NewSet(2, 3, 4, 5)

146
tools/src/cts/result/mvt.go Normal file
View File

@ -0,0 +1,146 @@
// Copyright 2022 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package result
import (
"sort"
"dawn.googlesource.com/dawn/tools/src/cts/query"
)
// MinimalVariantTags accepts a list of tag-sets (e.g GPU tags, OS tags, etc),
// and returns an optimized list of variants, folding together variants that
// have identical result query-to-status mappings, and removing redundant tags.
//
// MinimalVariantTags will attempt to remove variant tags starting with the
// first set of tags in tagSets, then second, and so on. If a tag-set cannot
// be removed, then the tags of the set are left alone, and the algorithm will
// progress to the next tag-set.
//
// MinimalVariantTags assumes that there are no duplicate results (same query,
// same tags) in l.
func (l List) MinimalVariantTags(tagSets []Tags) []Variant {
type VariantData struct {
// The variant tags
tags Variant
// The query -> status for all results in l that have this variant's
// tags.
queryToStatus map[query.Query]Status
}
variants := []VariantData{}
// Build the initial list of variants from l.
// Bin result [query -> status] to the variant.
{
variantIndices := map[string]int{}
for _, r := range l {
key := TagsToString(r.Tags)
if idx, found := variantIndices[key]; !found {
variantIndices[key] = len(variants)
variants = append(variants, VariantData{
tags: Variant(r.Tags.Clone()),
queryToStatus: map[query.Query]Status{
r.Query: r.Status,
},
})
} else {
variants[idx].queryToStatus[r.Query] = r.Status
}
}
}
// canReduce checks that the variant would match the same results if the
// tags were reduced to 'tags'. Returns true if the variant's tags could
// be reduced, otherwise false.
canReduce := func(variant VariantData, tags Tags) bool {
for _, r := range l.FilterByTags(tags) {
existing, found := variant.queryToStatus[r.Query]
if !found {
// Removing the tag has expanded the set of queries.
return false
}
if existing != r.Status {
// Removing the tag has resulted in two queries with different
// results.
return false
}
}
return true
}
// tryToRemoveTags will remove all the tags in 'tags' from all variants
// iff doing so does not affect the set of results filtered by each variant.
// If it was possible to remove the tags, then variants that now have the
// same tags may be folded together, reducing the total number of variants.
tryToRemoveTags := func(tags Tags) {
newVariants := make([]VariantData, 0, len(variants))
for _, v := range variants {
// Does the variant even contain these tags?
if !v.tags.ContainsAny(tags) {
// Nope. Skip the canReduce() call, and keep the variant.
newVariants = append(newVariants, v)
continue
}
// Build the new set of tags with 'tags' removed.
newTags := v.tags.Clone()
newTags.RemoveAll(tags)
// Check wether removal of these tags affected the outcome.
if !canReduce(v, newTags) {
// Removing these tags resulted in differences.
return // Abort
}
newVariants = append(newVariants, VariantData{newTags, v.queryToStatus})
}
// Remove variants that are now subsets of others.
// Start by sorting the variants by number of tags.
// This ensures that the variants with fewer tags (fewer constraints)
// come first.
sort.Slice(newVariants, func(i, j int) bool {
return len(newVariants[i].tags) < len(newVariants[j].tags)
})
// Now check each variant's tags against the previous variant tags.
// As we've sorted, we know that supersets (fewer-tags) come before
// subsets (more-tags).
variants = []VariantData{}
nextVariant:
for i, v1 := range newVariants { // for variants 0..N
for _, v2 := range newVariants[:i] { // for variants 0..i
if v1.tags.ContainsAll(v2.tags) {
continue nextVariant // v1 is a subset of v2. Omit.
}
}
variants = append(variants, v1)
}
}
// Attempt to remove the tag sets from the variants, one by one.
for _, tags := range tagSets {
tryToRemoveTags(tags)
}
// Return the final set of unique variants
out := make([]Variant, len(variants))
for i, v := range variants {
out[i] = v.tags
}
return out
}

View File

@ -0,0 +1,117 @@
// Copyright 2022 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package result_test
import (
"fmt"
"testing"
"dawn.googlesource.com/dawn/tools/src/cts/result"
"dawn.googlesource.com/dawn/tools/src/utils"
"github.com/google/go-cmp/cmp"
)
func TestMinimalVariantTags(t *testing.T) {
type Test struct {
location string
results result.List
expect []result.Variant
}
for _, test := range []Test{
{ //////////////////////////////////////////////////////////////////////
location: utils.ThisLine(),
results: result.List{},
expect: []result.Variant{},
}, { ///////////////////////////////////////////////////////////////////
// Single variant, that can be entirely optimized away
location: utils.ThisLine(),
results: result.List{
{Query: Q("a:b,c:d,*"), Tags: T("a0", "b1", "c2"), Status: result.Pass},
},
expect: []result.Variant{T()},
}, { ///////////////////////////////////////////////////////////////////
// Multiple variants on the same query.
// Can also be entirely optimized away.
location: utils.ThisLine(),
results: result.List{
{Query: Q("a:b,c:d,*"), Tags: T("a0", "b1", "c2"), Status: result.Pass},
{Query: Q("a:b,c:d,*"), Tags: T("a1", "b2", "c0"), Status: result.Pass},
{Query: Q("a:b,c:d,*"), Tags: T("a2", "b1", "c0"), Status: result.Pass},
},
expect: []result.Variant{T()},
}, { ///////////////////////////////////////////////////////////////////
// Two variants where the 1st and 2nd tag-sets are redundant.
location: utils.ThisLine(),
results: result.List{
{Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
{Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c1"), Status: result.Failure},
},
expect: []result.Variant{T("c0"), T("c1")},
}, { ///////////////////////////////////////////////////////////////////
// Two variants where the 1st and 3rd tag-sets are redundant.
location: utils.ThisLine(),
results: result.List{
{Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
{Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c1"), Status: result.Failure},
{Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c1"), Status: result.Pass},
{Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c0"), Status: result.Failure},
},
expect: []result.Variant{T("b0"), T("b1")},
}, { ///////////////////////////////////////////////////////////////////
// Two variants where the 2nd and 3rd tag-sets are redundant.
location: utils.ThisLine(),
results: result.List{
{Query: Q("a:b,c:d,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
{Query: Q("a:b,c:d,*"), Tags: T("a1", "b1", "c1"), Status: result.Failure},
{Query: Q("a:b,c:d,*"), Tags: T("a0", "b1", "c1"), Status: result.Pass},
{Query: Q("a:b,c:d,*"), Tags: T("a1", "b0", "c0"), Status: result.Failure},
},
expect: []result.Variant{T("a0"), T("a1")},
}, { ///////////////////////////////////////////////////////////////////
// Check that variants aren't optimized to expand the set of results
// they target, even if results are uniform
location: utils.ThisLine(),
results: result.List{
{Query: Q("a:b,c:d0,*"), Tags: T("a0", "b0", "c0"), Status: result.Pass},
{Query: Q("a:b,c:d1,*"), Tags: T("a1", "b1", "c1"), Status: result.Pass},
},
expect: []result.Variant{T("c0"), T("c1")},
}, { ///////////////////////////////////////////////////////////////////
// Exercise the optimizations to skip checks on tag removals that
// aren't found in all variants
location: utils.ThisLine(),
results: result.List{
{Query: Q("a:b,c:d0,*"), Tags: T("a0"), Status: result.Pass},
{Query: Q("a:b,c:d1,*"), Tags: T("b0"), Status: result.Pass},
{Query: Q("a:b,c:d2,*"), Tags: T("c0"), Status: result.Pass},
},
expect: []result.Variant{T("a0"), T("b0"), T("c0")},
},
} {
preReduce := fmt.Sprint(test.results)
got := test.results.MinimalVariantTags([]result.Tags{
T("a0", "a1", "a2"),
T("b0", "b1", "b2"),
T("c0", "c1", "c2"),
})
postReduce := fmt.Sprint(test.results)
if diff := cmp.Diff(got, test.expect); diff != "" {
t.Errorf("%v MinimalVariantTags() diff:\n%v", test.location, diff)
}
if diff := cmp.Diff(preReduce, postReduce); diff != "" {
t.Errorf("%v MinimalVariantTags() modified original list:\n%v", test.location, diff)
}
}
}

View File

@ -97,9 +97,13 @@ func Parse(in string) (Result, error) {
// List is a list of results
type List []Result
// Returns the list of unique tags across all results.
func (l List) UniqueTags() []Tags {
tags := container.NewMap[string, Tags]()
// Variant is a collection of tags that uniquely identify a test
// configuration (e.g the combination of OS, GPU, validation-modes, etc).
type Variant = Tags
// Variants returns the list of unique tags (variants) across all results.
func (l List) Variants() []Variant {
tags := container.NewMap[string, Variant]()
for _, r := range l {
tags.Add(TagsToString(r.Tags), r.Tags)
}

View File

@ -91,7 +91,7 @@ func TestParseError(t *testing.T) {
}
}
func TestUniqueTags(t *testing.T) {
func TestVariants(t *testing.T) {
type Test struct {
results result.List
expect []result.Tags
@ -198,7 +198,7 @@ func TestUniqueTags(t *testing.T) {
},
},
} {
got := test.results.UniqueTags()
got := test.results.Variants()
if diff := cmp.Diff(got, test.expect); diff != "" {
t.Errorf("Results:\n%v\nUniqueTags() was not as expected:\n%v", test.results, diff)
}