github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/public/static/js/task_timing.js (about) 1 mciModule.controller('TaskTimingController', function($scope, $http, $window, $filter, $locationHash, mciTime, notificationService) { 2 $scope.currentProject = $window.activeProject; 3 // sort the task names for the current project 4 $scope.currentProject.task_names.sort() 5 $scope.currentProject.build_variants.sort(function(a,b){ 6 return (a.name < b.name) ? -1 : 1 7 }); 8 $scope.currentBV = ""; 9 $scope.currentTask = ""; 10 $scope.currentHover = -1; 11 12 var allTasksField = "All Tasks"; 13 var time_taken = "tt"; 14 var makespan = "makespan"; 15 var processing_time = "tpt"; 16 var repotracker_requester = "gitter_request"; 17 var patch_requester = "patch_request" 18 var nsPerMs = 1000000; 19 20 var initialHash = $locationHash.get(); 21 // TODO do we keep this? 22 if (initialHash.buildVariant) { 23 for (var i = 0; i < $scope.currentProject.build_variants.length; ++i) { 24 if ($scope.currentProject.build_variants[i].name === initialHash.buildVariant) { 25 $scope.currentBV = $scope.currentProject.build_variants[i]; 26 } 27 } 28 } else { 29 // check if there are no build variants in the project 30 if ($scope.currentProject.build_variants != []) { 31 $scope.currentBV = $scope.currentProject.build_variants[0]; 32 } 33 } 34 35 // try to get the task from the current build variant, 36 // if there isn't one then get the first task 37 if(initialHash.taskName){ 38 $scope.currentTask = initialHash.taskName 39 } else { 40 if ($scope.currentProject.task_names != []) { 41 if ($scope.currentBV.task_names != []) { 42 $scope.currentTask = $scope.currentBV.task_names[0]; 43 } else { 44 $scope.currentTask = $scope.currentProject.task_names[0]; 45 } 46 } 47 } 48 49 50 51 $scope.taskData = {}; 52 $scope.locked = false; 53 $scope.hoverInfo = {hidden: true}; 54 $scope.allTasks = false; 55 56 $scope.requestViewOptions = [ 57 { 58 name: "Commits", 59 requester: repotracker_requester 60 }, 61 { 62 name: "Patches", 63 requester : patch_requester 64 } 65 ]; 66 67 // use the location hash for patches vs commit view 68 if (initialHash.requester) { 69 r = initialHash.requester; 70 if (r == repotracker_requester){ 71 $scope.currentRequest = $scope.requestViewOptions[0]; 72 } 73 if (r == patch_requester){ 74 $scope.currentRequest = $scope.requestViewOptions[1]; 75 } 76 } else { 77 $scope.currentRequest = $scope.requestViewOptions[0]; 78 } 79 80 $scope.setCurrentRequest = function(requestView) { 81 $scope.currentRequest = requestView; 82 $scope.load() 83 } 84 85 // normal task options 86 $scope.timeDiffOptions = [ 87 { 88 name: "Start \u21E2 Finish", 89 diff: ["finish_time", "start_time"] 90 91 }, 92 { 93 name: "Scheduled \u21E2 Start", 94 diff: ["start_time", "scheduled_time"] 95 }, 96 { 97 name: "Scheduled \u21E2 Finish", 98 diff: ["finish_time", "scheduled_time"] 99 } 100 ]; 101 102 $scope.timeDiff = $scope.timeDiffOptions[0]; 103 104 $scope.setTimeDiff = function(diff) { 105 $scope.timeDiff = diff; 106 $scope.load(); 107 } 108 109 // options for the all tasks functionality 110 $scope.allTasksOptions = [ 111 { 112 name: "Makespan", 113 type: makespan, 114 115 }, 116 { name: "Total Processing Time", 117 type: processing_time, 118 }, 119 ]; 120 $scope.allTasksView = $scope.allTasksOptions[0]; 121 122 123 $scope.setAllTasksView = function(view) { 124 $scope.allTasksView = view; 125 $scope.load(); 126 } 127 128 $scope.isAllTasks = function(){ 129 return $scope.allTasks || $scope.currentTask == "All Tasks"; 130 } 131 132 133 if (initialHash.limit) { 134 $scope.numTasks = initialHash.limit; 135 } else { 136 $scope.numTasks = 50; 137 } 138 $scope.numTasksOptions = [25, 50, 100, 200, 500, 1000, 2000]; 139 140 141 $scope.setNumTasks = function(num){ 142 $scope.numTasks = num; 143 $scope.load(); 144 } 145 146 // add an all tasks field 147 $scope.currentProject.task_names.unshift(allTasksField); 148 149 150 151 $scope.setBuildVariant = function(bv) { 152 $scope.currentBV = bv; 153 $scope.load(); 154 155 }; 156 157 $scope.setTaskName = function(task) { 158 if (task == allTasksField){ 159 $scope.allTasks = true; 160 $scope.currentTask = task; 161 $scope.load(); 162 return 163 } 164 $scope.allTasks = false; 165 $scope.currentTask = task; 166 $scope.load(); 167 } 168 169 170 // check that task is in list of build variants 171 $scope.checkTaskForGraph = function (task) { 172 if (task == allTasksField){ 173 return true; 174 } 175 return _.some($scope.currentBV.task_names, function(name){ return name == task}); 176 } 177 178 179 $scope.getLink = function() { 180 if ($scope.isAllTasks()) { 181 return "/build/" + $scope.hoverInfo.id; 182 } else { 183 return "/task/" + $scope.hoverInfo.id; 184 } 185 } 186 187 // filter function to remove zero times from a list of times 188 var nonZeroTimeFilter = function(y){return (+y) != (+new Date(0))} 189 190 var isPatch = function(){ 191 return $scope.currentRequest.requester == patch_requester; 192 } 193 194 var getHoverInfo = function(i){ 195 var hoverInfo; 196 if (isPatch()){ 197 hoverInfo = { 198 "revision" : $scope.versions[i].Revision, 199 "duration" : formatDuration(yMap($scope.taskData[i])), 200 "id" : $scope.taskData[i].id, 201 "message" : $scope.versions[i].Description, 202 "author": $scope.versions[i].Author, 203 "create_time": $scope.versions[i].CreateTime, 204 "hidden": false 205 } 206 } else { 207 hoverInfo = { 208 "revision" : $scope.versions[i].revision, 209 "duration" : formatDuration(yMap($scope.taskData[i])), 210 "id" : $scope.taskData[i].id, 211 "message" : $scope.versions[i].message, 212 "author": $scope.versions[i].author, 213 "create_time": $scope.versions[i].create_time, 214 "hidden": false 215 } 216 } 217 if (!$scope.isAllTasks()){ 218 hoverInfo.host = $scope.taskData[i].host 219 hoverInfo.distro = $scope.taskData[i].distro 220 } 221 return hoverInfo 222 } 223 224 function calculateMakespan(build) { 225 var tasks = build.tasks; 226 if (!tasks) { 227 return 0; 228 } 229 // extract the start an end times for the tasks in the build, discarding the zero times 230 var taskStartTimes = _.filter(_.pluck(tasks, "start_time").map(function(x){return new Date(x)}), nonZeroTimeFilter).sort(); 231 var taskEndTimes = _.filter(tasks.map(function(x){ 232 if(x.time_taken == 0 || +new Date(x.start_time) == +new Date(0)){ 233 return new Date(0); 234 }else { 235 return new Date((+new Date(x.start_time)) + (x.time_taken/nsPerMs)); 236 } 237 }), nonZeroTimeFilter).sort(); 238 239 240 if(taskStartTimes.length == 0 || taskEndTimes.length == 0) { 241 return 0; 242 } else { 243 var makeSpan = taskEndTimes[taskEndTimes.length-1] - taskStartTimes[0]; 244 if (makeSpan < 0) { 245 return 0; 246 } 247 return makeSpan; 248 } 249 250 } 251 252 function calculateTotalProcessingTime (build) { 253 var tasks = _.filter(build.tasks, function(task){return nonZeroTimeFilter(new Date(task.start_time));}) 254 return mciTime.fromNanoseconds(_.reduce(tasks, function(sum, task){return sum + task.time_taken}, 0)); 255 } 256 257 258 var xMap = function(task){ 259 return moment(task.create_time); 260 } 261 262 var yMap = function(task){ 263 if ($scope.isAllTasks()){ 264 if ($scope.allTasksView.type == makespan) { 265 return calculateMakespan(task); 266 } else { 267 return calculateTotalProcessingTime(task); 268 } 269 } 270 var a1 = moment(task[$scope.timeDiff.diff[0]]); 271 var a2 = moment(task[$scope.timeDiff.diff[1]]); 272 return mciTime.fromMilliseconds(a1.diff(a2)); 273 } 274 275 // isValidDate checks that none of the start, finish or scheduled times are zero 276 var isValidDate = function(task){ 277 return nonZeroTimeFilter(+new Date(task.start_time)) && nonZeroTimeFilter(+new Date(task.finish_time)) && nonZeroTimeFilter(+new Date(task.scheduled_time)) 278 } 279 280 281 $scope.load = function(before) { 282 $scope.hoverInfo.hidden = true; 283 $scope.locked = false; 284 // check that task exists in build variant 285 if (!$scope.checkTaskForGraph($scope.currentTask) && !$scope.isAllTasks()){ 286 return; 287 } 288 289 var limit = $scope.numTasks; 290 var query = (!!before ? 'before=' + encodeURIComponent(before) : ''); 291 query += (query.length > 0 && !!limit ? '&' : ''); 292 query += (!!limit ? 'limit=' + encodeURIComponent(limit) : ''); 293 url = '/json/task_timing/' + 294 encodeURIComponent($scope.currentProject.name) + '/' + 295 encodeURIComponent($scope.currentBV.name) + '/' + 296 encodeURIComponent($scope.currentRequest.requester) + '/' + 297 encodeURIComponent($scope.currentTask) 298 $http.get( 299 url + '?' + query). 300 success(function(data) { 301 $scope.taskData = ($scope.isAllTasks()) ? data.builds.reverse() : data.tasks.reverse(); 302 $scope.versions = ($scope.currentRequest.requester == repotracker_requester) ? data.versions.reverse() : data.patches.reverse(); 303 $scope.versions = _.filter($scope.versions, function(v, i){ 304 return isValidDate($scope.taskData[i]); 305 }) 306 $scope.taskData = _.filter($scope.taskData, isValidDate) 307 setTimeout(function(){$scope.drawDetailGraph()},0); 308 }). 309 error(function(data) { 310 notificationService.pushNotification("Error loading data: `" + data.error+"`", 'errorHeader', "error"); 311 $scope.taskData = []; 312 }); 313 314 setTimeout( function(p, bv, t, r, l){ 315 return function(){ 316 $locationHash.set({ project: p, buildVariant: bv, taskName: t, requester:r, limit: l}); 317 } 318 }($scope.currentProject.name, $scope.currentBV.name, $scope.currentTask, $scope.currentRequest.requester, $scope.numTasks), 0) 319 }; 320 321 // formatting function for the way the y values should show up. 322 // TODO: figure out how to make this look better... 323 var formatDuration = function(d) { 324 if (d == 0) { 325 return "0s" 326 } 327 return $filter('stringifyNanoseconds')(d * nsPerMs, true, true) 328 } 329 330 $scope.drawDetailGraph = function(){ 331 var graphId = "#tt-graph" 332 $(graphId).empty(); 333 334 // get the width of the column div for the width of the graph 335 var graph = d3.select(graphId)[0][0]; 336 var colWidth = graph.clientWidth; 337 338 var margin = { top: 20, right: 60, bottom: 100, left: 110 }; 339 var width = colWidth - margin.left - margin.right; 340 var height = (colWidth/2) - margin.top - margin.bottom; 341 var svg = d3.select(graphId) 342 .append("svg") 343 .attr("width", width + margin.left + margin.right) 344 .attr("height", height + margin.top + margin.bottom) 345 .append("g") 346 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 347 348 349 // sort task data by create time 350 $scope.taskData.sort(function(a,b){ 351 return moment(a.create_time).diff(moment(b.create_time)); 352 }) 353 354 355 356 var maxTime = d3.max($scope.taskData, function(task){return yMap(task);}); 357 var minTime = d3.min($scope.taskData, function(task){ return yMap(task);}); 358 359 var yScale = d3.scale.linear() 360 .domain([minTime, maxTime]) 361 .range([height, 0]); 362 363 var xScale = d3.scale.linear() 364 .domain([0, $scope.taskData.length - 1 ]) 365 .range([0, width]) 366 367 var yAxis = d3.svg.axis() 368 .scale(yScale) 369 .orient("left") 370 .tickFormat(formatDuration) 371 .ticks(10) 372 .tickSize(-width) 373 374 var xAxis = d3.svg.axis() 375 .scale(xScale) 376 .orient("bottom") 377 .ticks($scope.taskData.length) 378 .tickFormat(function(d, i){ 379 // every 5 ticks 380 if ($scope.numTasks >= 200 && i % 5 != 0) { 381 return ""; 382 } 383 // every 2 ticks 384 if ($scope.numTasks >= 100 && i % 2 != 0){ 385 return ""; 386 } 387 var task = $scope.taskData[d]; 388 if (task) { 389 return moment(task.create_time).format("M/D")} 390 return "" 391 }) 392 .tickSize(10) 393 394 // Define the line 395 var valueline = d3.svg.line() 396 .x(function(d, i) { return xScale(i);}) 397 .y(function(d) { return yScale(yMap(d)); }); 398 399 svg.append("g") 400 .attr("class", "x axis") 401 .attr("transform", "translate(0," + height + ")") 402 .call(xAxis); 403 svg.append("g") 404 .attr("class", "y axis") 405 .call(yAxis); 406 407 // now rotate text on x axis 408 svg.selectAll(".x text") // select all the text elements for the xaxis 409 .attr("transform", function(d) { 410 return "translate(" + this.getBBox().height*-2 + "," + (this.getBBox().height ) + ")rotate(-45)"; 411 }) 412 .attr("font-size", 10) 413 414 415 var scaledX = function(x, i){return xScale(i);} 416 var scaledY = function(y){return yScale(yMap(y));} 417 418 // Add the valueline path. 419 svg.append("path") 420 .attr("class", "line") 421 .attr("d", valueline($scope.taskData)); 422 423 // add the tooltip area to the webpage 424 var tooltip = d3.select("body").append("div") 425 .attr("class", "tooltip") 426 .style("opacity", 0); 427 428 var radius = $scope.numTasks >= 100 ? 1 : 3.5; 429 // draw dots 430 svg.selectAll(".task-circle") 431 .data($scope.taskData) 432 .enter().append("circle") 433 .attr("class", "task-circle") 434 .attr("r", radius) 435 .attr("cx", scaledX) 436 .attr("cy", scaledY) 437 .style("stroke", function(task){ 438 return task.status == 'success' ? 'green' : 'red'; 439 }) 440 441 // create a focus circle 442 var focus = svg.append("circle") 443 .attr("r", radius + 1); 444 445 446 // create a transparent rectangle for tracking hovering 447 svg.append("rect") 448 .attr("class", "overlay") 449 .attr("y", 0) 450 .attr("width", width) 451 .attr("height", height + margin.top) 452 .on("mouseover", function() { 453 focus.style("display", null); 454 }) 455 .on("mouseout", function() { 456 focus.style("display", "none"); 457 }) 458 // lock the scope if you click in the rectangle 459 .on("click", function(scope){ 460 return function(){ 461 scope.locked = !scope.locked 462 scope.$digest() 463 } 464 }($scope)) 465 .on("mousemove", function(){ 466 if($scope.locked){ 467 return; 468 } 469 var i = Math.round(xScale.invert(d3.mouse(this)[0])); 470 // if the revision is already there then return 471 if(i == $scope.currentHover){ 472 return 473 } 474 475 // set the revision to be the current hash 476 $scope.currentHover = i; 477 $scope.hoverInfo = getHoverInfo(i); 478 479 // set the focus's location to be the location of the closest point 480 focus.attr("cx", scaledX($scope.taskData[i], i)) 481 .attr("cy", scaledY($scope.taskData[i])); 482 $scope.$digest(); 483 }); 484 } 485 486 $scope.load(); 487 });