Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(200)

Side by Side Diff: infra/bots/gen_tasks.go

Issue 2386663002: [task scheduler] Add gen_tasks.go (Closed)
Patch Set: Share upload logic Created 4 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | infra/bots/tasks.json » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package main
6
7 /*
8 Generate the tasks.json file.
9 */
10
11 import (
12 "bytes"
13 "encoding/json"
14 "fmt"
15 "io/ioutil"
16 "os"
17 "path"
18 "path/filepath"
19 "sort"
20 "strings"
21
22 "github.com/skia-dev/glog"
23 "go.skia.org/infra/go/common"
24 "go.skia.org/infra/go/util"
25 "go.skia.org/infra/task_scheduler/go/specs"
26 )
27
28 const (
29 DEFAULT_OS = "Ubuntu"
30
31 // Pool for Skia bots.
32 POOL_SKIA = "Skia"
33
34 // Name prefix for upload jobs.
35 PREFIX_UPLOAD = "Upload"
36 )
37
38 var (
39 // "Constants"
40
41 // Top-level list of all jobs to run at each commit.
42 JOBS = []string{
43 "Build-Ubuntu-GCC-x86_64-Release-GN",
44 "Perf-Ubuntu-GCC-GCE-CPU-AVX2-x86_64-Release-GN",
45 "Test-Ubuntu-GCC-GCE-CPU-AVX2-x86_64-Release-GN",
46 }
47
48 // UPLOAD_DIMENSIONS are the Swarming dimensions for upload tasks.
49 UPLOAD_DIMENSIONS = []string{
50 "cpu:x86-64-avx2",
51 "gpu:none",
52 "os:Ubuntu",
53 fmt.Sprintf("pool:%s", POOL_SKIA),
54 }
55
56 // Defines the structure of job names.
57 jobNameSchema *JobNameSchema
58
59 // Caches CIPD package info so that we don't have to re-read VERSION
60 // files.
61 cipdPackages = map[string]*specs.CipdPackage{}
62
63 // Path to the infra/bots directory.
64 infrabotsDir = ""
65 )
66
67 // deriveCompileTaskName returns the name of a compile task based on the given
68 // job name.
69 func deriveCompileTaskName(jobName string, parts map[string]string) string {
70 if parts["role"] == "Housekeeper" {
71 return "Build-Ubuntu-GCC-x86_64-Release-Shared"
72 } else if parts["role"] == "Test" || parts["role"] == "Perf" {
73 task_os := parts["os"]
74 ec := parts["extra_config"]
75 if task_os == "Android" {
76 if ec == "Vulkan" {
77 ec = "Android_Vulkan"
78 } else if !strings.Contains(ec, "GN_Android") {
79 ec = task_os
80 }
81 task_os = "Android"
82 } else if task_os == "iOS" {
83 ec = task_os
84 task_os = "Mac"
85 } else if strings.Contains(task_os, "Win") {
86 task_os = "Win"
87 }
88 name, err := jobNameSchema.MakeJobName(map[string]string{
89 "role": "Build",
90 "os": task_os,
91 "compiler": parts["compiler"],
92 "target_arch": parts["arch"],
93 "configuration": parts["configuration"],
94 "extra_config": ec,
95 })
96 if err != nil {
97 glog.Fatal(err)
98 }
99 return name
100 } else {
101 return jobName
102 }
103 }
104
105 // swarmDimensions generates swarming bot dimensions for the given task.
106 func swarmDimensions(parts map[string]string) []string {
107 if parts["extra_config"] == "SkiaCT" {
108 return []string{
109 "pool:SkiaCT",
110 }
111 }
112 d := map[string]string{
113 "pool": POOL_SKIA,
114 }
115 if os, ok := parts["os"]; ok {
116 d["os"] = os
117 } else {
118 d["os"] = DEFAULT_OS
119 }
120 if strings.Contains(d["os"], "Win") {
121 d["os"] = "Windows"
122 }
123 if parts["role"] == "Test" || parts["role"] == "Perf" {
124 if strings.Contains(parts["os"], "Android") {
125 // For Android, the device type is a better dimension
126 // than CPU or GPU.
127 d["device_type"] = map[string]string{
128 "AndroidOne": "sprout",
129 "GalaxyS3": "m0", // "smdk4x12", Detected i ncorrectly by swarming?
130 "GalaxyS4": "", // TODO(borenet,kjlubick)
131 "GalaxyS7": "heroqlteatt",
132 "NVIDIA_Shield": "foster",
133 "Nexus10": "manta",
134 "Nexus5": "hammerhead",
135 "Nexus6": "shamu",
136 "Nexus6p": "angler",
137 "Nexus7": "grouper",
138 "Nexus7v2": "flo",
139 "Nexus9": "flounder",
140 "NexusPlayer": "fugu",
141 }[parts["model"]]
142 } else if strings.Contains(parts["os"], "iOS") {
143 d["device"] = map[string]string{
144 "iPad4": "iPad4,1",
145 }[parts["model"]]
146 // TODO(borenet): Replace this hack with something
147 // better.
148 d["os"] = "iOS-9.2"
149 } else if parts["cpu_or_gpu"] == "CPU" {
150 d["gpu"] = "none"
151 d["cpu"] = map[string]string{
152 "AVX": "x86-64",
153 "AVX2": "x86-64-avx2",
154 "SSE4": "x86-64",
155 }[parts["cpu_or_gpu_value"]]
156 if strings.Contains(parts["os"], "Win") && parts["cpu_or _gpu_value"] == "AVX2" {
157 // AVX2 is not correctly detected on Windows. Fa ll back on other
158 // dimensions to ensure that we correctly target machines which we know
159 // have AVX2 support.
160 d["cpu"] = "x86-64"
161 d["os"] = "Windows-2008ServerR2-SP1"
162 }
163 } else {
164 d["gpu"] = map[string]string{
165 "GeForce320M": "10de:08a4",
166 "GT610": "10de:104a",
167 "GTX550Ti": "10de:1244",
168 "GTX660": "10de:11c0",
169 "GTX960": "10de:1401",
170 "HD4000": "8086:0a2e",
171 "HD4600": "8086:0412",
172 "HD7770": "1002:683d",
173 "iHD530": "8086:1912",
174 }[parts["cpu_or_gpu_value"]]
175 }
176 } else {
177 d["gpu"] = "none"
178 }
179 rv := make([]string, 0, len(d))
180 for k, v := range d {
181 rv = append(rv, fmt.Sprintf("%s:%s", k, v))
182 }
183 sort.Strings(rv)
184 return rv
185 }
186
187 // getCipdPackage finds and returns the given CIPD package and version.
188 func getCipdPackage(assetName string) *specs.CipdPackage {
189 if pkg, ok := cipdPackages[assetName]; ok {
190 return pkg
191 }
192 versionFile := path.Join(infrabotsDir, "assets", assetName, "VERSION")
193 contents, err := ioutil.ReadFile(versionFile)
194 if err != nil {
195 glog.Fatal(err)
196 }
197 version := strings.TrimSpace(string(contents))
198 pkg := &specs.CipdPackage{
199 Name: fmt.Sprintf("skia/bots/%s", assetName),
200 Path: assetName,
201 Version: fmt.Sprintf("version:%s", version),
202 }
203 if assetName == "win_toolchain" {
204 pkg.Path = "t" // Workaround for path length limit on Windows.
205 }
206 cipdPackages[assetName] = pkg
207 return pkg
208 }
209
210 // compile generates a compile task. Returns the name of the last task in the
211 // generated chain of tasks, which the Job should add as a dependency.
212 func compile(cfg *specs.TasksCfg, name string, parts map[string]string) string {
213 // Collect the necessary CIPD packages.
214 pkgs := []*specs.CipdPackage{}
215
216 // Android bots require a toolchain.
217 if strings.Contains(name, "Android") {
218 pkgs = append(pkgs, getCipdPackage("android_sdk"))
219 if strings.Contains(name, "Mac") {
220 pkgs = append(pkgs, getCipdPackage("android_ndk_darwin") )
221 } else {
222 pkgs = append(pkgs, getCipdPackage("android_ndk_linux"))
223 }
224 }
225
226 // Clang on Linux.
227 if strings.Contains(name, "Ubuntu") && strings.Contains(name, "Clang") {
228 pkgs = append(pkgs, getCipdPackage("clang_linux"))
229 }
230
231 // Windows toolchain.
232 if strings.Contains(name, "Win") {
233 pkgs = append(pkgs, getCipdPackage("win_toolchain"))
234 if strings.Contains(name, "Vulkan") {
235 pkgs = append(pkgs, getCipdPackage("win_vulkan_sdk"))
236 }
237 }
238
239 // Add the task.
240 cfg.Tasks[name] = &specs.TaskSpec{
241 CipdPackages: pkgs,
242 Dimensions: swarmDimensions(parts),
243 ExtraArgs: []string{
244 "--workdir", "../../..", "swarm_compile",
245 fmt.Sprintf("buildername=%s", name),
246 "mastername=fake-master",
247 "buildnumber=2",
248 "slavename=fake-buildslave",
249 fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLAT ED_OUTDIR),
250 fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
251 },
252 Isolate: "compile_skia.isolate",
253 Priority: 0.8,
254 }
255 return name
256 }
257
258 // recreateSKPs generates a RecreateSKPs task. Returns the name of the last
259 // task in the generated chain of tasks, which the Job should add as a
260 // dependency.
261 func recreateSKPs(cfg *specs.TasksCfg, name string) string {
262 // TODO
263 return name
264 }
265
266 // ctSKPs generates a CT SKPs task. Returns the name of the last task in the
267 // generated chain of tasks, which the Job should add as a dependency.
268 func ctSKPs(cfg *specs.TasksCfg, name string) string {
269 // TODO
270 return name
271 }
272
273 // housekeeper generates a Housekeeper task. Returns the name of the last task
274 // in the generated chain of tasks, which the Job should add as a dependency.
275 func housekeeper(cfg *specs.TasksCfg, name, compileTaskName string) string {
276 // TODO
277 return name
278 }
279
280 // doUpload indicates whether the given Job should upload its results.
281 func doUpload(name string) bool {
282 skipUploadBots := []string{
283 "ASAN",
284 "Coverage",
285 "MSAN",
286 "TSAN",
287 "UBSAN",
288 "Valgrind",
289 }
290 for _, s := range skipUploadBots {
291 if strings.Contains(name, s) {
292 return false
293 }
294 }
295 return true
296 }
297
298 // test generates a Test task. Returns the name of the last task in the
299 // generated chain of tasks, which the Job should add as a dependency.
300 func test(cfg *specs.TasksCfg, name string, parts map[string]string, compileTask Name string, pkgs []*specs.CipdPackage) string {
301 cfg.Tasks[name] = &specs.TaskSpec{
302 CipdPackages: pkgs,
303 Dependencies: []string{compileTaskName},
304 Dimensions: swarmDimensions(parts),
305 ExtraArgs: []string{
306 "--workdir", "../../..", "swarm_test",
307 fmt.Sprintf("buildername=%s", name),
308 "mastername=fake-master",
309 "buildnumber=2",
310 "slavename=fake-buildslave",
311 fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLAT ED_OUTDIR),
312 fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
313 },
314 Isolate: "test_skia.isolate",
315 Priority: 0.8,
316 }
317 // Upload results if necessary.
318 if doUpload(name) {
319 uploadName := fmt.Sprintf("%s%s%s", PREFIX_UPLOAD, jobNameSchema .Sep, name)
320 cfg.Tasks[uploadName] = &specs.TaskSpec{
321 Dependencies: []string{name},
322 Dimensions: UPLOAD_DIMENSIONS,
323 ExtraArgs: []string{
324 "--workdir", "../../..", "upload_dm_results",
325 fmt.Sprintf("buildername=%s", name),
326 "mastername=fake-master",
327 "buildnumber=2",
328 "slavename=fake-buildslave",
329 fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDE R_ISOLATED_OUTDIR),
330 fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REV ISION),
331 },
332 Isolate: "upload_dm_results.isolate",
333 Priority: 0.8,
334 }
335 return uploadName
336 }
337 return name
338 }
339
340 // perf generates a Perf task. Returns the name of the last task in the
341 // generated chain of tasks, which the Job should add as a dependency.
342 func perf(cfg *specs.TasksCfg, name string, parts map[string]string, compileTask Name string, pkgs []*specs.CipdPackage) string {
343 cfg.Tasks[name] = &specs.TaskSpec{
344 CipdPackages: pkgs,
345 Dependencies: []string{compileTaskName},
346 Dimensions: swarmDimensions(parts),
347 ExtraArgs: []string{
348 "--workdir", "../../..", "swarm_perf",
349 fmt.Sprintf("buildername=%s", name),
350 "mastername=fake-master",
351 "buildnumber=2",
352 "slavename=fake-buildslave",
353 fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLAT ED_OUTDIR),
354 fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
355 },
356 Isolate: "perf_skia.isolate",
357 Priority: 0.8,
358 }
359 // Upload results if necessary.
360 if strings.Contains(name, "Release") && doUpload(name) {
361 uploadName := fmt.Sprintf("%s%s%s", PREFIX_UPLOAD, jobNameSchema .Sep, name)
362 cfg.Tasks[uploadName] = &specs.TaskSpec{
363 Dependencies: []string{name},
364 Dimensions: UPLOAD_DIMENSIONS,
365 ExtraArgs: []string{
366 "--workdir", "../../..", "upload_nano_results",
367 fmt.Sprintf("buildername=%s", name),
368 "mastername=fake-master",
369 "buildnumber=2",
370 "slavename=fake-buildslave",
371 fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDE R_ISOLATED_OUTDIR),
372 fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REV ISION),
373 },
374 Isolate: "upload_nano_results.isolate",
375 Priority: 0.8,
376 }
377 return uploadName
378 }
379 return name
380 }
381
382 // process generates tasks and jobs for the given job name.
383 func process(cfg *specs.TasksCfg, name string) {
384 if _, ok := cfg.Jobs[name]; ok {
385 glog.Fatalf("Duplicate job %q", name)
386 }
387 deps := []string{}
388
389 parts, err := jobNameSchema.ParseJobName(name)
390 if err != nil {
391 glog.Fatal(err)
392 }
393
394 // RecreateSKPs.
395 if strings.Contains(name, "RecreateSKPs") {
396 deps = append(deps, recreateSKPs(cfg, name))
397 }
398
399 // CT bots.
400 if strings.Contains(name, "-CT_") {
401 deps = append(deps, ctSKPs(cfg, name))
402 }
403
404 // Compile bots.
405 if parts["role"] == "Build" {
406 deps = append(deps, compile(cfg, name, parts))
407 }
408
409 // Any remaining bots need a compile task.
410 compileTaskName := deriveCompileTaskName(name, parts)
411
412 // Housekeeper.
413 if parts["role"] == "Housekeeper" {
414 deps = append(deps, housekeeper(cfg, name, compileTaskName))
415 }
416
417 // Common assets needed by the remaining bots.
418 pkgs := []*specs.CipdPackage{
419 getCipdPackage("skimage"),
420 getCipdPackage("skp"),
421 getCipdPackage("svg"),
422 }
423
424 // Test bots.
425 if parts["role"] == "Test" {
426 deps = append(deps, test(cfg, name, parts, compileTaskName, pkgs ))
427 }
428
429 // Perf bots.
430 if parts["role"] == "Perf" {
431 deps = append(deps, perf(cfg, name, parts, compileTaskName, pkgs ))
432 }
433
434 // Add the Job spec.
435 cfg.Jobs[name] = &specs.JobSpec{
436 Priority: 0.8,
437 TaskSpecs: deps,
438 }
439 }
440
441 // getCheckoutRoot returns the path of the root of the Skia checkout, or an
442 // error if it cannot be found.
443 func getCheckoutRoot() string {
444 cwd, err := os.Getwd()
445 if err != nil {
446 glog.Fatal(err)
447 }
448 for {
449 if _, err := os.Stat(cwd); err != nil {
450 glog.Fatal(err)
451 }
452 s, err := os.Stat(path.Join(cwd, ".git"))
453 if err == nil && s.IsDir() {
454 // TODO(borenet): Should we verify that this is a Skia
455 // checkout and not something else?
456 return cwd
457 }
458 cwd = filepath.Clean(path.Join(cwd, ".."))
459 }
460 }
461
462 // Regenerate the tasks.json file.
463 func main() {
464 common.Init()
465 defer common.LogPanic()
466
467 // Where are we?
468 root := getCheckoutRoot()
469 infrabotsDir = path.Join(root, "infra", "bots")
470
471 // Create the JobNameSchema.
472 schema, err := NewJobNameSchema(path.Join(infrabotsDir, "recipe_modules" , "builder_name_schema", "builder_name_schema.json"))
473 if err != nil {
474 glog.Fatal(err)
475 }
476 jobNameSchema = schema
477
478 // Create the config.
479 cfg := &specs.TasksCfg{
480 Jobs: map[string]*specs.JobSpec{},
481 Tasks: map[string]*specs.TaskSpec{},
482 }
483
484 // Create Tasks and Jobs.
485 for _, j := range JOBS {
486 process(cfg, j)
487 }
488
489 // Validate the config.
490 if err := cfg.Validate(); err != nil {
491 glog.Fatal(err)
492 }
493
494 // Write the tasks.json file.
495 outFile := path.Join(root, specs.TASKS_CFG_FILE)
496 b, err := json.MarshalIndent(cfg, "", " ")
497 if err != nil {
498 glog.Fatal(err)
499 }
500 // The json package escapes HTML characters, which makes our output
501 // much less readable. Replace the escape characters with the real
502 // character.
503 b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1)
504 if err := ioutil.WriteFile(outFile, b, os.ModePerm); err != nil {
505 glog.Fatal(err)
506 }
507 }
508
509 // TODO(borenet): The below really belongs in its own file, probably next to the
510 // builder_name_schema.json file.
511
512 // JobNameSchema is a struct used for (de)constructing Job names in a
513 // predictable format.
514 type JobNameSchema struct {
515 Schema map[string][]string `json:"builder_name_schema"`
516 Sep string `json:"builder_name_sep"`
517 }
518
519 // NewJobNameSchema returns a JobNameSchema instance based on the given JSON
520 // file.
521 func NewJobNameSchema(jsonFile string) (*JobNameSchema, error) {
522 var rv JobNameSchema
523 f, err := os.Open(jsonFile)
524 if err != nil {
525 return nil, err
526 }
527 defer util.Close(f)
528 if err := json.NewDecoder(f).Decode(&rv); err != nil {
529 return nil, err
530 }
531 return &rv, nil
532 }
533
534 // ParseJobName splits the given Job name into its component parts, according
535 // to the schema.
536 func (s *JobNameSchema) ParseJobName(n string) (map[string]string, error) {
537 split := strings.Split(n, s.Sep)
538 if len(split) < 2 {
539 return nil, fmt.Errorf("Invalid job name: %q", n)
540 }
541 role := split[0]
542 split = split[1:]
543 keys, ok := s.Schema[role]
544 if !ok {
545 return nil, fmt.Errorf("Invalid job name; %q is not a valid role .", role)
546 }
547 extraConfig := ""
548 if len(split) == len(keys)+1 {
549 extraConfig = split[len(split)-1]
550 split = split[:len(split)-1]
551 }
552 if len(split) != len(keys) {
553 return nil, fmt.Errorf("Invalid job name; %q has incorrect numbe r of parts.", n)
554 }
555 rv := make(map[string]string, len(keys)+2)
556 rv["role"] = role
557 if extraConfig != "" {
558 rv["extra_config"] = extraConfig
559 }
560 for i, k := range keys {
561 rv[k] = split[i]
562 }
563 return rv, nil
564 }
565
566 // MakeJobName assembles the given parts of a Job name, according to the schema.
567 func (s *JobNameSchema) MakeJobName(parts map[string]string) (string, error) {
568 role, ok := parts["role"]
569 if !ok {
570 return "", fmt.Errorf("Invalid job parts; jobs must have a role. ")
571 }
572 keys, ok := s.Schema[role]
573 if !ok {
574 return "", fmt.Errorf("Invalid job parts; unknown role %q", role )
575 }
576 rvParts := make([]string, 0, len(parts))
577 rvParts = append(rvParts, role)
578 for _, k := range keys {
579 v, ok := parts[k]
580 if !ok {
581 return "", fmt.Errorf("Invalid job parts; missing %q", k )
582 }
583 rvParts = append(rvParts, v)
584 }
585 if _, ok := parts["extra_config"]; ok {
586 rvParts = append(rvParts, parts["extra_config"])
587 }
588 return strings.Join(rvParts, s.Sep), nil
589 }
OLDNEW
« no previous file with comments | « no previous file | infra/bots/tasks.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698