SkQP: consolidate cut_release scripts

replace the following scripts: cut_release, get_gold_results.py,
goldgetter.py, make_rendertests_list.py, and upload_model with a single
program: cut_release.  Still depends on three C++ programs: jitter_gms,
list_gpu_unit_tests, and make_skqp_model.

Change-Id: I28f59bc1f0caedc05d6ce2c4cc11bbd66cfb9784
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/209171
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Hal Canary <halcanary@google.com>
diff --git a/tools/skqp/README_GENERATING_MODELS.md b/tools/skqp/README_GENERATING_MODELS.md
index b36ba54..b852564 100644
--- a/tools/skqp/README_GENERATING_MODELS.md
+++ b/tools/skqp/README_GENERATING_MODELS.md
@@ -8,21 +8,19 @@
 
         COMMIT=origin/master
 
-1.  Get the positively triaged results from Gold:
+    Or use the script to find the best one:
+
+        cd SKIA_SOURCE_DIRECTORY
+        git fetch origin
+        COMMIT=$(python tools/skqp/find_commit_with_best_gold_results.py \
+                 origin/master ^origin/skqp/dev)
+
+1.  Get the positively triaged results from Gold and generate models:
 
         cd SKIA_SOURCE_DIRECTORY
         git fetch origin
         git checkout "$COMMIT"
-        python tools/skqp/get_gold_results.py "${COMMIT}~10" "$COMMIT"
-
-    This will produce a file `meta_YYYMMMDDD_HHHMMMSS_COMMIT_COMMIT.json` in
-    the current directory.
-
-2.  From a checkout of Skia's master branch, execute:
-
-        cd SKIA_SOURCE_DIRECTORY
-        git checkout "$COMMIT"
-        tools/skqp/cut_release META_JSON_FILE
+        python tools/skqp/cut_release.py HEAD~10 HEAD
 
     This will create the following files:
 
@@ -32,9 +30,9 @@
 
     These three files can be commited to Skia to create a new commit.  Make
     `origin/skqp/dev` a parent of this commit (without merging it in), and
-    push this new commit to `origin/skqp/dev`:
+    push this new commit to `origin/skqp/dev`, using this script:
 
-        tools/skqp/branch_skqp_dev.sh
+        sh tools/skqp/branch_skqp_dev.sh
 
     Review and submit the change:
 
@@ -47,20 +45,18 @@
 
     (Optional) Test the SkQP APK:
 
-        adb uninstall org.skia.skqp
-        tools/skqp/test_apk.sh LOCATION/skqp-universal-debug.apk
+        tools/skqp/test_apk.sh (LOCATION)/skqp-universal-debug.apk
 
     (Once changes land) Upload the SkQP APK.
 
-        tools/skqp/upload_apk LOCATION/skqp-universal-debug.apk
+        tools/skqp/upload_apk HEAD (LOCATION)/skqp-universal-debug.apk
 
 
-`tools/skqp/cut_release`
-------------------------
+`tools/skqp/cut_release.py`
+---------------------------
 
-This tool will call `make_gmkb.go` to generate the `m{ax,in}.png` files for
-each render test.  Additionaly, a `models.txt` file enumerates all of the
-models.
+This tool will call `make_skqp_model` to generate the `m{ax,in}.png` files for
+each render test.
 
 Then it calls `jitter_gms` to see which render tests pass the jitter test.
 `jitter_gms` respects the `bad_gms.txt` file by ignoring the render tests
@@ -70,7 +66,7 @@
 Next, the `skqp/rendertests.txt` file is created.  This file lists the render
 tests that will be executed by SkQP.  These are the union of the tests
 enumerated in the `good.txt` and `bad.txt` files.  If the render test is found
-in the `models.txt` file and the `good.txt` file, its per-test threshold is set
+in the `good.txt` file and the model exists, its per-test threshold is set
 to 0 (a later CL can manually change this, if needed).  Otherwise, the
 threshold is set to -1; this indicated that the rendertest will be executed (to
 verify that the driver will not crash), but the output will not be compared
diff --git a/tools/skqp/cut_release b/tools/skqp/cut_release
deleted file mode 100755
index a6b199c..0000000
--- a/tools/skqp/cut_release
+++ /dev/null
@@ -1,26 +0,0 @@
-#! /bin/sh
-# Copyright 2018 Google LLC.
-# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
-
-if [ -z "$1" ]; then
-    echo "Usage: $0 META.JSON" >&2
-    exit 1
-fi
-
-set -x
-set -e
-META_JSON="$1"
-cd "$(dirname "$0")/../.."
-env GIT_SYNC_DEPS_QUIET=1 python tools/git-sync-deps
-O='out/ndebug'
-mkdir -p $O
-bin/gn gen $O --args='cc="clang" cxx="clang++" is_debug=false'
-ninja -C $O jitter_gms list_gpu_unit_tests make_skqp_model
-GMKB='platform_tools/android/apps/skqp/src/main/assets/gmkb'
-python tools/skqp/goldgetter.py "$META_JSON" "$GMKB" $O/make_skqp_model
-$O/jitter_gms tools/skqp/bad_gms.txt
-python tools/skqp/make_rendertests_list.py
-rm 'bad.txt' 'good.txt' "$GMKB"/models.txt
-sh tools/skqp/upload_model
-$O/list_gpu_unit_tests \
-    > platform_tools/android/apps/skqp/src/main/assets/skqp/unittests.txt
diff --git a/tools/skqp/cut_release.py b/tools/skqp/cut_release.py
new file mode 100755
index 0000000..fba4f6c
--- /dev/null
+++ b/tools/skqp/cut_release.py
@@ -0,0 +1,168 @@
+#! /usr/bin/env python
+# Copyright 2018 Google LLC.
+# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+
+import json
+import md5
+import multiprocessing
+import os
+import shutil
+import sys
+import tempfile
+import urllib
+import urllib2
+
+from subprocess import check_call, check_output
+
+assert '/' in [os.sep, os.altsep] and os.pardir == '..'
+
+ASSETS = 'platform_tools/android/apps/skqp/src/main/assets'
+BUCKET = 'skia-skqp-assets'
+
+def make_skqp_model(arg):
+    name, urls, exe = arg
+    tmp = tempfile.mkdtemp()
+    for url in urls:
+        urllib.urlretrieve(url, tmp + '/' + url[url.rindex('/') + 1:])
+    check_call([exe, tmp, ASSETS + '/gmkb/' + name])
+    shutil.rmtree(tmp)
+    sys.stdout.write(name + ' ')
+    sys.stdout.flush()
+
+def goldgetter(meta, exe):
+    assert os.path.exists(exe)
+    jobs = []
+    for rec in meta:
+        urls = [d['URL'] for d in rec['digests']
+                if d['status'] == 'positive' and
+                (set(d['paramset']['config']) & set(['vk', 'gles']))]
+        if urls:
+            jobs.append((rec['testName'], urls, exe))
+    pool = multiprocessing.Pool(processes=20)
+    pool.map(make_skqp_model, jobs)
+    sys.stdout.write('\n')
+    return set((n for n, _, _ in jobs))
+
+def gold(first_commit, last_commit):
+    c1, c2 = (check_output(['git', 'rev-parse', c]).strip()
+            for c in (first_commit, last_commit))
+    f = urllib2.urlopen('https://public-gold.skia.org/json/export?' + urllib.urlencode([
+        ('fbegin', c1),
+        ('fend', c2),
+        ('query', 'config=gles&config=vk&source_type=gm'),
+        ('pos', 'true'),
+        ('neg', 'false'),
+        ('unt', 'false')
+    ]))
+    j = json.load(f)
+    f.close()
+    return j
+
+def gset(path):
+    s = set()
+    if os.path.isfile(path):
+        with open(path, 'r') as f:
+            for line in f:
+                s.add(line.strip())
+    return s
+
+def make_rendertest_list(models, good, bad):
+    assert good.isdisjoint(bad)
+    do_score = good & models
+    no_score = bad | (good - models)
+    to_delete = models & bad
+    for d in to_delete:
+        path = ASSETS + '/gmkb/' + d
+        if os.path.isdir(path):
+            shutil.rmtree(path)
+    results = dict()
+    for n in do_score:
+        results[n] = 0
+    for n in no_score:
+        results[n] = -1
+    return ''.join('%s,%d\n' % (n, results[n]) for n in sorted(results))
+
+def get_digest(path):
+    m = md5.new()
+    with open(path, 'r') as f:
+        m.update(f.read())
+    return m.hexdigest()
+
+def upload_cmd(path, digest):
+    return ['gsutil', 'cp', path, 'gs://%s/%s' % (BUCKET, digest)]
+
+def upload_model():
+    bucket_url = 'gs://%s/' % BUCKET
+    extant = set((u.replace(bucket_url, '', 1)
+                  for u in check_output(['gsutil', 'ls', bucket_url]).splitlines() if u))
+    cmds = []
+    filelist = []
+    for dirpath, _, filenames in os.walk(ASSETS + '/gmkb'):
+        for filename in filenames:
+            path = os.path.join(dirpath, filename)
+            digest = get_digest(path)
+            if digest not in extant:
+                cmds.append(upload_cmd(path, digest))
+            filelist.append('%s;%s\n' % (digest, os.path.relpath(path, ASSETS)))
+    tmp = tempfile.mkdtemp()
+    filelist_path = tmp + '/x'
+    with open(filelist_path, 'w') as o:
+        for l in filelist:
+            o.write(l)
+    filelist_digest = get_digest(filelist_path)
+    if filelist_digest not in extant:
+        cmds.append(upload_cmd(filelist_path, filelist_digest))
+
+    pool = multiprocessing.Pool(processes=20)
+    pool.map(check_call, cmds)
+    shutil.rmtree(tmp)
+    return filelist_digest
+
+def remove(x):
+    if os.path.isdir(x) and not os.path.islink(x):
+        shutil.rmtree(x)
+    if os.path.exists(x):
+        os.remove(x)
+
+def main(first_commit, last_commit):
+    check_call(upload_cmd('/dev/null', get_digest('/dev/null')))
+
+    os.chdir(os.path.dirname(__file__) + '/../..')
+    remove(ASSETS + '/files.checksum')
+    for d in [ASSETS + '/gmkb', ASSETS + '/skqp', ]:
+        remove(d)
+        os.mkdir(d)
+
+    check_call([sys.executable, 'tools/git-sync-deps'],
+               env=dict(os.environ, GIT_SYNC_DEPS_QUIET='T'))
+    build = 'out/ndebug'
+    check_call(['bin/gn', 'gen', build,
+                '--args=cc="clang" cxx="clang++" is_debug=false'])
+    check_call(['ninja', '-C', build,
+                'jitter_gms', 'list_gpu_unit_tests', 'make_skqp_model'])
+
+    models = goldgetter(gold(first_commit, last_commit), build + '/make_skqp_model')
+
+    check_call([build + '/jitter_gms', 'tools/skqp/bad_gms.txt'])
+
+    with open(ASSETS + '/skqp/rendertests.txt', 'w') as o:
+        o.write(make_rendertest_list(models, gset('good.txt'), gset('bad.txt')))
+
+    remove('good.txt')
+    remove('bad.txt')
+
+    with open(ASSETS + '/skqp/unittests.txt', 'w') as o:
+        o.write(check_output([build + '/list_gpu_unit_tests']))
+
+    with open(ASSETS + '/files.checksum', 'w') as o:
+        o.write(upload_model() + '\n')
+
+    sys.stdout.write(ASSETS + '/files.checksum\n')
+    sys.stdout.write(ASSETS + '/skqp/rendertests.txt\n')
+    sys.stdout.write(ASSETS + '/skqp/unittests.txt\n')
+
+if __name__ == '__main__':
+    if len(sys.argv) != 3:
+        sys.stderr.write('Usage:\n  %s C1 C2\n\n' % sys.argv[0])
+        sys.exit(1)
+    main(sys.argv[1], sys.argv[2])
diff --git a/tools/skqp/get_gold_results.py b/tools/skqp/get_gold_results.py
deleted file mode 100755
index ebf23f2..0000000
--- a/tools/skqp/get_gold_results.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#! /usr/bin/env python
-
-# Copyright 2018 Google LLC.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import os
-import subprocess
-import sys
-import time
-import urllib
-
-def gold_export_url(first_commit, last_commit):
-    query = [
-        ('fbegin', first_commit),
-        ('fend',   last_commit),
-        ('query',  'config=gles&config=vk&source_type=gm'),
-        ('pos',    'true'),
-        ('neg',    'false'),
-        ('unt',    'false')
-    ]
-    return 'https://public-gold.skia.org/json/export?' + urllib.urlencode(query)
-
-def git_rev_parse(rev):
-    return subprocess.check_output(['git', 'rev-parse', rev]).strip()
-
-def main(args):
-    if len(args) != 2:
-        sys.stderr.write('Usage:\n  %s FIRST_COMMIT LAST_COMMIT\n' % __file__)
-        sys.exit(1)
-    c1 = git_rev_parse(args[0])
-    c2 = git_rev_parse(args[1])
-    now = time.strftime("%Y%m%d_%H%M%S", time.gmtime())
-    url = gold_export_url(c1, c2)
-    sys.stdout.write(url + '\n')
-    filename = 'meta_%s_%s_%s.json' % (now, c1[:16], c2[:16])
-    urllib.urlretrieve(url, filename)
-    sys.stdout.write('\n' + filename + '\n')
-
-if __name__ == '__main__':
-    main(sys.argv[1:])
-
diff --git a/tools/skqp/goldgetter.py b/tools/skqp/goldgetter.py
deleted file mode 100755
index ba58eb9..0000000
--- a/tools/skqp/goldgetter.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#! /usr/bin/env python
-# Copyright 2019 Google LLC.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import json
-import multiprocessing
-import os
-import shutil
-import subprocess
-import sys
-import tempfile
-import urllib
-
-def make_skqp_model(arg):
-    name, urls, dst_dir, exe = arg
-    tmp = tempfile.mkdtemp()
-    for url in urls:
-        urllib.urlretrieve(url, tmp + '/' + url[url.rindex('/') + 1:])
-    subprocess.check_call([exe, tmp, dst_dir + '/' + name])
-    shutil.rmtree(tmp)
-    sys.stdout.write(name + ' ')
-    sys.stdout.flush()
-
-def main(meta, dst, exe):
-    assert os.path.exists(exe)
-    jobs = []
-    with open(meta, 'r') as f:
-        for rec in json.load(f):
-            urls = [d['URL'] for d in rec['digests']
-                    if d['status'] == 'positive' and
-                    (set(d['paramset']['config']) & set(['vk', 'gles']))]
-            if urls:
-                jobs.append((rec['testName'], urls, dst, exe))
-    if not os.path.exists(dst):
-        os.mkdir(dst)
-    pool = multiprocessing.Pool(processes=20)
-    pool.map(make_skqp_model, jobs)
-    sys.stdout.write('\n')
-    with open(dst + '/models.txt', 'w') as o:
-        for n, _, _, _ in jobs:
-            o.write(n + '\n')
-
-if __name__ == '__main__':
-    if len(sys.argv) != 4:
-        sys.stderr.write('Usage:\n  %s META.JSON DST_DIR MAKE_SKQP_MODEL_EXE\n\n' % sys.argv[0])
-        sys.exit(1)
-    main(sys.argv[1], sys.argv[2], sys.argv[3])
diff --git a/tools/skqp/make_rendertests_list.py b/tools/skqp/make_rendertests_list.py
deleted file mode 100755
index 54d9203..0000000
--- a/tools/skqp/make_rendertests_list.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#! /usr/bin/env python
-
-# Copyright 2018 Google LLC.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import csv
-import os
-import shutil
-import sys
-
-def gset(path):
-    s = set()
-    if os.path.isfile(path):
-        with open(path, 'r') as f:
-            for line in f:
-                s.add(line.strip())
-    return s
-
-def main():
-    assert '/' in [os.sep, os.altsep]
-    assets = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
-                          'platform_tools/android/apps/skqp/src/main/assets')
-    models = gset(assets + '/gmkb/models.txt')
-    good = gset('good.txt')
-    bad = gset('bad.txt')
-    assert good.isdisjoint(bad)
-    do_score = good & models
-    no_score = bad | (good - models)
-    to_delete = models & bad
-    for d in to_delete:
-        path = assets + '/gmkb/' + d
-        if os.path.isdir(path):
-            shutil.rmtree(path)
-    results = dict()
-    for n in do_score:
-        results[n] = 0
-    for n in no_score:
-        results[n] = -1
-    skqp =  assets + '/skqp'
-    if not os.path.isdir(skqp):
-        os.mkdir(skqp)
-    with open(skqp + '/rendertests.txt', 'w') as o:
-        for n in sorted(results):
-            o.write('%s,%d\n' % (n, results[n]))
-
-if __name__ == '__main__':
-    main()
-
diff --git a/tools/skqp/upload_model b/tools/skqp/upload_model
deleted file mode 100755
index b1058e6..0000000
--- a/tools/skqp/upload_model
+++ /dev/null
@@ -1,62 +0,0 @@
-#! /bin/sh
-
-# Copyright 2018 Google Inc.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-set -e
-
-BASE_DIR='platform_tools/android/apps/skqp/src/main/assets'
-PATH_LIST='gmkb'
-BUCKET=skia-skqp-assets
-
-EXTANT="$(mktemp "${TMPDIR:-/tmp}/extant.XXXXXXXXXX")"
-gsutil ls gs://$BUCKET/ | sed "s|^gs://$BUCKET/||"  > "$EXTANT"
-
-upload() {
-    MD5=$(md5sum < "$1" | head -c 32)
-    if ! grep -q "$MD5" "$EXTANT"; then
-        URL="gs://${BUCKET}/$MD5"
-        gsutil cp "$1" "$URL" > /dev/null 2>&1 &
-    fi
-    echo $MD5
-}
-
-size() { gsutil du -s gs://$BUCKET | awk '{print $1}'; }
-
-cd "$(dirname "$0")/../../$BASE_DIR"
-
-rm -f files.checksum
-
-FILES="$(mktemp "${TMPDIR:-/tmp}/files.XXXXXXXXXX")"
-
-: > "$FILES"
-
-COUNT=$(find $PATH_LIST -type f | wc -l)
-INDEX=1
-SHARD_COUNT=32
-
-SIZE=$(size)
-find $PATH_LIST -type f | sort | while IFS= read -r FILENAME; do
-    printf '\r %d / %d   ' "$INDEX" "$COUNT"
-    if ! [ -f "$FILENAME" ]; then
-        echo error [${FILENAME}] >&2;
-        exit 1;
-    fi
-    case "$FILENAME" in *\;*) echo bad filename: $FILENAME >&2; exit 1;; esac
-    MD5=$(upload "$FILENAME")
-    printf '%s;%s\n' "$MD5" "$FILENAME" >> "$FILES"
-
-    if [ $(($INDEX % $SHARD_COUNT)) = 0 ]; then
-        wait
-    fi
-    INDEX=$(( $INDEX + 1))
-done
-printf '\rdone          \n'
-upload "$FILES" > files.checksum
-wait
-
-D=$(( $(size) - $SIZE ))
-printf 'Added %d bytes to %s, %d%%\n' $D $BUCKET $(( $D * 100 / $SIZE ))
-
-rm "$EXTANT"