정보
요청한 주제가 아래에 표시됩니다. 그러나 이 주제는 이 라이브러리에 포함되지 않습니다.

웹 작업자 활용

터치 사용에서 제공한 정보를 기반으로 여기서는 웹 작업자를 사용하여 성능을 향상시키는 방법에 대해 설명합니다.

참고  웹 작업자를 사용하는 웹 페이지는 HTTP 또는 HTTPS 프로토콜을 통해 제공되어야 합니다.

웹 작업자에서 설명한 대로 웹 작업자 API를 사용하면 웹 응용 프로그램 작성자가 기본 페이지와 함께 병렬로(동시에) 실행되는 백그라운드 스크립트를 생성할 수 있습니다. 장기 실행 작업에 사용할 여러 스레드를 동시에 생성할 수 있습니다. 새 작업자 개체에는 .js 파일이 필요하며, 이 파일은 웹 서버에 대한 비동기 요청을 통해 포함됩니다.


var myWorker = new Worker('worker.js');

작업자 스레드와의 모든 통신은 메시지를 통해 관리됩니다. 호스트 작업자와 작업자 스크립트 모두 postMessage를 사용하여 메시지를 보내고, onmessage 이벤트를 사용하여 응답을 수신 대기할 수 있습니다. 메시지 내용은 이벤트 개체의 data 속성으로서 전송됩니다.

이러한 이해에 기반한 알고리즘은 다음과 같습니다.

  1. 기본 페이지(사용자 인터페이스) 스레드에서 Mandelbrot 이미지를 생성하는 데 필요한 정보(canvas data array 형식)를 개체 리터럴에 배치합니다.
  2. postMessage를 사용하여 개체 리터럴을 작업자 스레드로 보내 처리합니다.
  3. 작업자 스레드에서 처리를 완료하면 완료된 캔버스 이미지 데이터 배열이 UI(사용자 인터페이스) 스레드로 다시 전송되어 캔버스에 표시됩니다.

이 알고리즘을 적용하기 위해 drawMandelbrot를 다음과 같이 수정합니다.


function drawMandelbrot(ReMax, ReMin, ImMax, ImMin, grayscaleFactor) {      
  var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
  var messageBox = document.getElementById('messageBox');     
  var elapsedTime =  document.getElementById('elapsedTime');     
  var canvas = globals.canvas; // A small speed optimization - accessing local variables tends to be faster than accessing global variables.
  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;
  var ctx = canvas.context;  
  var imageDataObject = ctx.imageDataObject; // imageDataObject ends up receiving an altered copy of ctx.imageDataObject, so imageDataObject is not a pointer to (reference to) ctx.imageDataObject.
  var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
  var fineDetailMandelbrotReceived = false; // Just in case the fine detail Web Worker callback finishes before the coarse detail Web Worker callback.

  messageBox.innerHTML = "Calculating..."; // This isn't displayed until the drawMandelbrot function block exits.
  elapsedTime.innerHTML = ""; // Erase the prior run's statistics.
          
  var workerMessage = {
    workerID: "",
    MAX_ITERATIONS: MAX_ITERATIONS,
    ReMax: ReMax,
    ReMin: ReMin,
    ImMax: ImMax,
    ImMin: ImMin,
    grayscaleFactor: grayscaleFactor,
    canvasWidth: canvasWidth,
    canvasHeight: canvasHeight,
    imageDataObject: imageDataObject
  };
  
  function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
    if (fineDetailMandelbrotReceived) {
      return; // For some reason, the fine detail callback finished before the coarse detail callback - do not display the coarse Mandelbrot image.
    }
    
    ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
    globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
    globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          
  
    var elapsedMilliseconds = (new Date()) - startTime;
    elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.iterationSum.format() + " iterations in " + (elapsedMilliseconds / 1000).toFixed(2) + " seconds"; // Note that the UI element is not updated until after this block terminates (which is the desired behavior).            
    
    if (evt.data.workerID == "Fine detail") {
      fineDetailMandelbrotReceived = true;
      messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
    } // if
  } // workerCallback
  
  globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
  globals.fineDetailWorker.onmessage = workerCallback;

  workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always been a (positive) integer.
  workerMessage.workerID = "Coarse detail";
  globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

  workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
  workerMessage.workerID = "Fine detail";                
  globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.
} // drawMandelbrot

작업자 스레드에서는 DOM에 액세스할 수 없으며 일반적으로 DOM을 참조하는 개체를 받을 수 없으므로 Mandelbrot 이미지를 생성하는 데 필요한 정보를 개체 리터럴에 배치합니다. 이 개체 리터럴은 UI 스레드에서 작업자 스레드로 전송됩니다.


var workerMessage = {
  workerID: "",
  MAX_ITERATIONS: MAX_ITERATIONS,
  ReMax: ReMax,
  ReMin: ReMin,
  ImMax: ImMax,
  ImMin: ImMin,
  grayscaleFactor: grayscaleFactor,
  canvasWidth: canvasWidth,
  canvasHeight: canvasHeight,
  imageDataObject: imageDataObject
};

imageDataObject 개체는 작업자 스레드에서 채워질 캔버스 이미지 데이터 배열을 제공합니다.

이러한 정보에 따라 Mandelbrot 8은 다음과 같이 표시됩니다.

Mandelbrot 8


<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="X-UA-Compatible" content="IE=10" />
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Mandelbrot 8</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      text-align: center;
    }
    
    canvas {
      border: 2px solid black;
      -ms-touch-action: none;
      touch-action: none; /* Use the standard, if available. */
    }    
    
    table {
      margin: 0 auto; /* Center the table. */
    }
    
    #messageBox {
      text-align: left;
    }
        
    #elapsedTime {
      width: 23em;
      text-align: right;
    }    

    button {
      width: 5em;
    }
    
    #filenameForm {
      visibility: hidden; /* As opposed to "display: none", keep room for this hidden element in the layout. */
    }
}
  </style>      
</head>

<body>
  <h1>Mandelbrot 8</h1>
  <table>
    <tr>
      <td id="messageBox"></td>
      <td id="elapsedTime"></td>
    </tr>
  </table>
  <canvas width="600" height="400" oncontextmenu="return false;"> <!-- Because the hold gesture event can fire more than once, the 'oncontextmenu="return false;"' attribute is used to stop the right-click context menu from appearing inappropriately. -->
    Canvas not supported - upgrade your browser (after checking that your browser is in the correct mode).
  </canvas><br>
  <button type="button" id="resetButton">Reset</button>  
  <button type="button" id="lightenButton">Lighten</button>    
  <button type="button" id="saveButton">Save</button>
  <form id="filenameForm"> 
    Extensionless filename: <input id="filename" type="text"> <input type="submit" value="Submit">
  </form>
  <script>
    if (!window.Worker) { // Check for the availability of the Worker() constructor.
      document.getElementsByTagName('body')[0].innerHTML = "<h2>Web Workers not supported - upgrade your browser<br>(after checking that your browser is in the correct mode)</h2>";      
    }
    else {
      var RE_MAX = 1.1; // This value will be adjusted as necessary to ensure that the rendered Mandelbrot set is never skewed (that is, true to it's actual shape).
      var RE_MIN = -2.5;
      var IM_MAX = 1.2;
      var IM_MIN = -1.2;
      var MAX_ITERATIONS = 1200; // Increase this value to improve detection of complex c values that belong to the Mandelbrot set.
      var STATIC_ZOOM_BOX_FACTOR = 0.25; // Increase to make the double-click and hold gesture zoom box bigger.
      var DEFAULT_MESSAGE = "Click or click-and-drag to zoom."      
      
      var globals = {}; // See the handleLoad function.
      
      window.addEventListener('load', handleLoad, false);
    } // if-else
                
    /************************************************************************************************************************************************************/
    
    Number.prototype.format = function() {
    /* 
      Formats this integer so that it has commas in the expected places.
    */
    	var numberString = Math.round(this).toString(); // An integer value is assumed, so we ensure that it is indeed an integer.
    	var precompiledRegularExpression = /(\d+)(\d{3})/;
    	
    	while ( precompiledRegularExpression.test(numberString) ) {
    		numberString = numberString.replace(precompiledRegularExpression, '$1' + ',' + '$2'); // For this integer, inject ","'s at the appropriate locations.
    	} // while
    	
    	return numberString;
    } // Number.prototype.format

    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    

    function handleLoad() {          
      var canvas = document.getElementsByTagName('canvas')[0];
      var canvasWidth = canvas.width;
      var canvasHeight = canvas.height;      
      var ctx = canvas.getContext('2d');
      
      document.getElementsByTagName('table')[0].width = canvasWidth; // Make the table's width the same as the canvas's width. 
      document.getElementById('messageBox').innerHTML = DEFAULT_MESSAGE;            

      globals.canvas = canvas;
      globals.canvas.context = ctx;
      globals.canvas.context.imageDataObject = ctx.createImageData(canvasWidth, canvasHeight); // Create an appropriately sized but empty canvas image data object.
      
      globals.staticZoomBoxWidth = STATIC_ZOOM_BOX_FACTOR * canvasWidth; // Maintains the original canvas width/height ratio.
      globals.staticZoomBoxHeight = STATIC_ZOOM_BOX_FACTOR * canvasHeight; // Maintains the original canvas width/height ratio.      
      
      globals.pointer = {};
      globals.pointer.down = false;  
                 
      window.addEventListener('hashchange', handleHashChange, false); // This event handler executes whenever the URL hash string changes.
      
      if (window.navigator.pointerEnabled || window.navigator.msPointerEnabled) { // Future proofing.
        // It's either-or with MS pointer events - they cannot be registered concurrently.
        window.gesture = window.gesture || window.MSGesture; // Future proofing.
        globals.gesture = new gesture();
        globals.gesture.target = canvas; 
        canvas.addEventListener('MSPointerDown', function(evt) { globals.gesture.addPointer(evt.pointerId); }, false); 
          
        canvas.addEventListener('MSGestureStart', handlePointer, false); 
        canvas.addEventListener('mousedown', handlePointer, false); // Required for the case when the mouse is clicked but not moved.
        
        canvas.addEventListener('MSGestureChange', handlePointer, false);
        
        canvas.addEventListener('MSGestureEnd', handlePointer, false);
        canvas.addEventListener('mouseup', handlePointer, false); // Required for the case when the mouse is clicked but not moved.

        canvas.addEventListener('MSGestureHold', handlePointer, false);
      }    
      else {
        canvas.addEventListener('mousedown', handlePointer, false);
        canvas.addEventListener('mousemove', handlePointer, false);
        canvas.addEventListener('mouseup', handlePointer, false);    
      } // if-else
            
      document.getElementById('resetButton').addEventListener('click', handleResetButton, false);
      document.getElementById('lightenButton').addEventListener('click', handleLightenButton, false);    
      document.getElementById('saveButton').addEventListener('click', handleSaveButton, false);        
      document.getElementById('filenameForm').addEventListener('submit', handleFormSubmit, false);    
      
      ctx.fillStyle = "rgba(255, 0, 0, 0.3)"; // The color and opacity of the zoom box. This is what gets saved when calling ctx.save().          
 
      handleHashChange(); // On page load, simulate a page URL change to draw the initial Mandelbrot set.
    } // handleLoad
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    
    
    function handleHashChange() {
      var hashValues = getHashValues(); // This function examines window.location.hash but doesn't change it.
      
      if (hashValues) {
        globals.ReMax = hashValues.ReMax;
        globals.ReMin = hashValues.ReMin;
        globals.ImMax = hashValues.ImMax;
        globals.ImMin = hashValues.ImMin;
        globals.grayscaleFactor = hashValues.grayscaleFactor;
      }
      else {
        globals.ReMax = adjusted_RE_MAX();
        globals.ReMin = RE_MIN;
        globals.ImMax = IM_MAX;
        globals.ImMin = IM_MIN;     
        globals.grayscaleFactor = 1; // Multiplying any value by 1 has no effect.
      } // if-else
      
      initializeWebWorkers('mandelbrotWebWorker.js'); // Halt any in-process Web Workers so that the back/forward buttons behave as expected (i.e., deal with the asynchronous nature of the Web Workers).
      drawMandelbrot(globals.ReMax, globals.ReMin, globals.ImMax, globals.ImMin, globals.grayscaleFactor);
    } // handelHashChange    

    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/        

    function getHashValues() {
      var dirtyComplexPlaneExtremaString = (window.location.hash).replace('#', ''); // Remove the leading "#" character from the string.
      var complexPlaneExtremaString = dirtyComplexPlaneExtremaString.split(','); // Returns an array. Assumes the following string form: "ReMax,ReMin,ImMax,ImMin,grayscaleFactor" (note that if grayscaleFactor is 1, the image's grayscale is not affected).
      
      var ReMax = parseFloat( complexPlaneExtremaString[0] ); 
      var ReMin = parseFloat( complexPlaneExtremaString[1] ); 
      var ImMax = parseFloat( complexPlaneExtremaString[2] ); 
      var ImMin = parseFloat( complexPlaneExtremaString[3] );
      var grayscaleFactor = parseFloat( complexPlaneExtremaString[4] );
      
      if ( isNaN(ReMax) || isNaN(ReMin) || isNaN(ImMax) || isNaN(ImMin) || isNaN(grayscaleFactor) ) { 
        return null;
      } // if 
      
      return {ReMax: ReMax, ReMin: ReMin, ImMax: ImMax, ImMin: ImMin, grayscaleFactor: grayscaleFactor};
    } // getHashValues
        
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/        

    function adjusted_RE_MAX() {    
      var ReMax = globals.canvas.width * ( (IM_MAX - IM_MIN) / globals.canvas.height ) + RE_MIN;
      
      if (RE_MAX != ReMax) {
        alert("RE_MAX has been adjusted to: " + ReMax); // The user should never see this if RE_MAX is set correctly above.
      } // if

      return ReMax;
    } // adjusted_RE_MAX    
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    
    
    function drawMandelbrot(ReMax, ReMin, ImMax, ImMin, grayscaleFactor) {      
      var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
      var messageBox = document.getElementById('messageBox');     
      var elapsedTime =  document.getElementById('elapsedTime');     
      var canvas = globals.canvas; // A small speed optimization - accessing local variables tends to be faster than accessing global variables.
      var canvasWidth = canvas.width;
      var canvasHeight = canvas.height;
      var ctx = canvas.context;  
      var imageDataObject = ctx.imageDataObject; // imageDataObject ends up receiving an altered copy of ctx.imageDataObject, so imageDataObject is not a pointer to (reference to) ctx.imageDataObject.
      var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
      var fineDetailMandelbrotReceived = false; // Just in case the fine detail Web Worker callback finishes before the coarse detail Web Worker callback.

      messageBox.innerHTML = "Calculating..."; // This isn't displayed until the drawMandelbrot function block exits.
      elapsedTime.innerHTML = ""; // Erase the prior run's statistics.
              
      var workerMessage = {
        workerID: "",
        MAX_ITERATIONS: MAX_ITERATIONS,
        ReMax: ReMax,
        ReMin: ReMin,
        ImMax: ImMax,
        ImMin: ImMin,
        grayscaleFactor: grayscaleFactor,
        canvasWidth: canvasWidth,
        canvasHeight: canvasHeight,
        imageDataObject: imageDataObject
      };
      
      function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
        if (fineDetailMandelbrotReceived) {
          return; // For some reason, the fine detail callback finished before the coarse detail callback - do not display the coarse Mandelbrot image.
        }
        
        ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
        globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
        globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          
      
        var elapsedMilliseconds = (new Date()) - startTime;
        elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.iterationSum.format() + " iterations in " + (elapsedMilliseconds / 1000).toFixed(2) + " seconds"; // Note that the UI element is not updated until after this block terminates (which is the desired behavior).            
        
        if (evt.data.workerID == "Fine detail") {
          fineDetailMandelbrotReceived = true;
          messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
        } // if
      } // workerCallback
      
      globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
      globals.fineDetailWorker.onmessage = workerCallback;

      workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always been a (positive) integer.
      workerMessage.workerID = "Coarse detail";
      globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

      workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
      workerMessage.workerID = "Fine detail";                
      globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.
    } // drawMandelbrot
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function xToRe(x) {
      var x_coefficient = (globals.ReMax - globals.ReMin) / globals.canvas.width; 
      
      return (x * x_coefficient) + globals.ReMin; // Converts a canvas x-coordinate value to the associated complex plane Re-coordinate.
    } // xToRe
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    

    function yToIm(y) {
      var y_coefficient = (globals.ImMin - globals.ImMax) / globals.canvas.height; 
      
      return (y * y_coefficient) + globals.ImMax; // Converts a canvas y-coordinate value to the associated complex plane Im-coordinate.
    } // yToIm
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/

    function handlePointer(evt) {
      var canvasWidthHeightRatio = globals.canvas.width / globals.canvas.height;
      var ctx = globals.canvas.context;
      
      var canvasX;
      var canvasY;      
      
      if (evt.offsetX && evt.offsetY) {
        canvasX = evt.offsetX; // Not supported in Firefox.
        canvasY = evt.offsetY; // Does not assume that the canvas element is a direct descendent of the body element.
      } else {
        canvasX = evt.clientX - evt.target.offsetLeft; // Supported in Firefox.
        canvasY = evt.clientY - evt.target.offsetTop; // Assumes that the canvas element is a direct descendent of the body element.
      } // if-else
      
      var zoomBoxWidth;
      var zoomBoxHeight;
      
      var ReMax;
      var ReMin;
      var ImMax;
      var ImMin;
      
      var staticZoomBoxWidth = globals.staticZoomBoxWidth;
      var staticZoomBoxHeight = globals.staticZoomBoxHeight;
      var halfStaticZoomBoxWidth = staticZoomBoxWidth / 2;
      var halfStaticZoomBoxHeight = staticZoomBoxHeight / 2;
      
      switch (evt.type) {
        case 'MSGestureStart':              
        case 'mousedown':
          globals.pointer.down = true;      
          globals.pointer.x1 = canvasX;
          globals.pointer.y1 = canvasY;
          break;
        case 'MSGestureChange':                  
        case 'mousemove':
          if (globals.pointer.down) {
            zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1);  
            zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // We must keep the zoom box dimensions proportional to the canvas dimensions in order to ensure that the resulting zoomed Mandelbrot image does not become skewed.
            ctx.putImageData(ctx.imageDataObject, 0, 0); // Assumes that an initial image of the Mandelbrot set is drawn before we get to this point in the code. The purpose of this line is to erase the prior zoom box rectangle before drawing the next zoom box rectangle.
            ctx.fillRect(globals.pointer.x1, globals.pointer.y1, zoomBoxWidth, zoomBoxHeight); // With a freshly painted image of the current Mandelbrot set in place (see prior line), draw a new zoom box rectangle.
          }
          break;
        case 'MSGestureEnd':
        case 'mouseup':
          globals.pointer.down = false;          
          
          zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1); // Only allow the zoom box to be drawn from an upper-left corner position down to a lower-right corner position.
          zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // Again, ensure that the width/height ratio of the zoom box is proportional to the canvas's (this simplifies the algorithm).          
          
          if (zoomBoxHeight == 0) { // No zoom box has been drawn, so honor the fixed sized zoom box.  
            ctx.putImageData(ctx.imageDataObject, 0, 0); // For the MSGestureHold case, erase the previously drawn zoom box so we don't draw two or more on top of each other.
            ctx.fillRect(canvasX - halfStaticZoomBoxWidth, canvasY - halfStaticZoomBoxHeight, staticZoomBoxWidth, staticZoomBoxHeight); // Just leave this on the screen.
                         
            ReMin = xToRe(canvasX - halfStaticZoomBoxWidth); // Center the static zoom box about the point (evt.offsetX, evt.offsetY).
            ImMax = yToIm(canvasY - halfStaticZoomBoxHeight); 
            
            ReMax = xToRe(canvasX + halfStaticZoomBoxWidth);
            ImMin = yToIm(canvasY + halfStaticZoomBoxHeight);
          } 
          else { // A (possibly tiny) zoom box has been drawn, so honor it.
            ReMin = xToRe(globals.pointer.x1); // Convert the mouse's x-coordinate value (on the canvas) to the associated Re-coordinate value in the complex plane.
            ImMax = yToIm(globals.pointer.y1); // Convert the mouse's y-coordinate value (on the canvas) to the associated Im-coordinate value in the complex plane.
                                      
            ReMax = xToRe(zoomBoxWidth + globals.pointer.x1); // Convert the zoom box's final x-coordinate value to the associated Re-coordinate value in the complex plane.  
            ImMin = yToIm(zoomBoxHeight + globals.pointer.y1);  // Convert the zoom box's final y-coordinate value to the associated Re-coordinate value in the complex plane.            
          } // if-else        
        
          window.location.hash = ReMax + "," + ReMin + "," + ImMax + "," + ImMin + "," + globals.grayscaleFactor; // This triggers the handleHashChange event handler which, among other things, is responsible for drawing the Mandelbrot set.
          break; 
        case 'MSGestureHold':
          if (evt.detail & evt.MSGESTURE_FLAG_BEGIN) {
            ctx.fillRect(canvasX - halfStaticZoomBoxWidth, canvasY - halfStaticZoomBoxHeight, staticZoomBoxWidth, staticZoomBoxHeight); // At the first sign of a hold gesture, get the zoom box up on the screen immediately.                 
          }  
          
          // The evt.MSGESTURE_FLAG_END component of the hold gesture is handled by the "if (zoomBoxHeight == 0)" clause of the MSGestureEnd clause above.
          
          break;
        default:
          alert("Error in switch statement."); // Although unnecessary, defensive programming techniques such as this are highly recommended.
      } // switch              
    } // handlePointer    
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function handleResetButton() {
      window.location.hash = adjusted_RE_MAX() + "," + RE_MIN + "," + IM_MAX + "," + IM_MIN + "," + 1; // // This triggers the handleHashChange event handler which, among other things, is responsible for drawing the Mandelbrot set.
    } // handleResetButton
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function handleLightenButton() {
    /* 
      This creates a value (factor) such that black (0) stays black and the lightest gray value in the image becomes white (255). Thus, clicking the 
      Lighten button can remove mathematical meaning of the (proper) grayscale but can make dark images more visible.
    */
      var grayscaleFactor = 255 / globals.maxPixelGrayscaleValue; // For the canvas element, 255 is white, 0 is black.

      window.location.hash = globals.ReMax + "," + globals.ReMin + "," + globals.ImMax + "," + globals.ImMin + "," + grayscaleFactor; // This invokes handleHashChange which, among other things, is responsibile for drawing the Mandelbrot set.
    } // handleResetButton
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/

    function handleSaveButton() {
      document.getElementById('filenameForm').style.visibility = "visible";
      document.getElementById('filename').focus(); // Place the cursor in the filename text input box.
    } // handleResetButton 
 
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/

    function handleFormSubmit(evt) {
    /* 
      As of 8/2012, the following code only works starting with Internet Explorer 10.
    */
      evt.preventDefault(); // Do not refresh the page when the Submit button is clicked.
      
      window.BlobBuilder = window.BlobBuilder || window.MSBlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
      globals.canvas.toBlob = globals.canvas.toBlob || globals.canvas.msToBlob;      
      window.navigator.saveBlob = window.navigator.saveBlob || window.navigator.msSaveBlob;
      
      if (window.BlobBuilder && globals.canvas.toBlob && window.navigator.saveBlob) {
        var extensionlessFilename = evt.target[0].value;
        var filename = extensionlessFilename + ".png";
        var blobBuilderObject = new BlobBuilder(); // Create a blob builder object so that we can append content to it.
                
        blobBuilderObject.append( globals.canvas.toBlob() ); // Append the user's drawing in PNG format to the builder object.
        window.navigator.saveBlob(blobBuilderObject.getBlob(), filename); // Move the builder object content to a blob and save it to a file.      
      }
      else {
        document.getElementById('messageBox').innerHTML = "<strong style='color: red;'>CANNOT SAVE FILE</strong>";
      } // if-else
      
      document.getElementById('filenameForm').style.visibility = "hidden";
    } // handleFormSubmit

    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/

    function initializeWebWorkers(webWorkerJsPath) {
      if (globals.coarseDetailWorker) {
        globals.coarseDetailWorker.terminate();      
      }
      
      if (globals.fineDetailWorker) {
        globals.fineDetailWorker.terminate();      
      }
      
      globals.coarseDetailWorker = new Worker(webWorkerJsPath);
      globals.fineDetailWorker = new Worker(webWorkerJsPath);
    } // initializeWebWorkers
  </script>
</body>

</html>

시작할 때 웹 작업자를 사용할 수 없으면 즉시 중단합니다. 그렇지 않으면 이전과 같이 초기화합니다.


if (!window.Worker) { // Check for the availability of the Worker() constructor.
  document.getElementsByTagName('body')[0].innerHTML = "<h2>Web Workers not supported - upgrade your browser<br>(after checking that your browser is in the correct mode)</h2>";      
}
else {
  var RE_MAX = 1.1; // This value will be adjusted as necessary to ensure that the rendered Mandelbrot set is never skewed (that is, true to it's actual shape).
  var RE_MIN = -2.5;
  var IM_MAX = 1.2;
  var IM_MIN = -1.2;
  var MAX_ITERATIONS = 1200; // Increase this value to improve detection of complex c values that belong to the Mandelbrot set.
  var STATIC_ZOOM_BOX_FACTOR = 0.25; // Increase to make the double-click and hold gesture zoom box bigger.
  var DEFAULT_MESSAGE = "Click or click-and-drag to zoom."      
  
  var globals = {}; // See the handleLoad function.
  
  window.addEventListener('load', handleLoad, false);
} // if-else

handleHashChange에서 initializeWebWorkers('mandelbrotWebWorker.js')를 호출합니다. mandelbrotWebWorker.js 파일은 작업자 스레드에서 실행할 코드를 정의합니다. initializeWebWorkers 함수 자체는 다음과 같이 표시됩니다.


function initializeWebWorkers(webWorkerJsPath) {
  if (globals.coarseDetailWorker) {
    globals.coarseDetailWorker.terminate();      
  }
  
  if (globals.fineDetailWorker) {
    globals.fineDetailWorker.terminate();      
  }
  
  globals.coarseDetailWorker = new Worker(webWorkerJsPath);
  globals.fineDetailWorker = new Worker(webWorkerJsPath);
} // initializeWebWorkers

두 개의 if 문은 현재 Mandelbrot 이미지가 완전히 렌더링되기 전에 사용자가 뒤로 또는 앞으로 단추를 클릭하는 경우에 필요한 in-process 작업자 스레드를 종료하는 데 사용됩니다.

마지막 두 줄에서는 두 개의 동일한 작업자 개체(즉, 둘 다 webWorkerJsPath에 있는 동일한 코드 실행)를 포함하는 전역 변수 두 개를 만듭니다.

페이지가 로드되면 handleLoad가 실행되어 handleHashChange를 호출합니다. handleHashChangeinitializeWebWorkers('mandelbrotWebWorker.js')(위에서 설명)를 호출한 다음 drawMandelbrot를 호출하며, 다음과 같이 두 개의 동시 작업자 스레드를 호출하도록 수정되었습니다.


function drawMandelbrot(ReMax, ReMin, ImMax, ImMin, grayscaleFactor) {      
  var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
  var messageBox = document.getElementById('messageBox');     
  var elapsedTime =  document.getElementById('elapsedTime');     
  var canvas = globals.canvas; // A small speed optimization - accessing local variables tends to be faster than accessing global variables.
  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;
  var ctx = canvas.context;  
  var imageDataObject = ctx.imageDataObject; // imageDataObject ends up receiving an altered copy of ctx.imageDataObject, so imageDataObject is not a pointer to (reference to) ctx.imageDataObject.
  var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
  var fineDetailMandelbrotReceived = false; // Just in case the fine detail Web Worker callback finishes before the coarse detail Web Worker callback.

  messageBox.innerHTML = "Calculating..."; // This isn't displayed until the drawMandelbrot function block exits.
  elapsedTime.innerHTML = ""; // Erase the prior run's statistics.
          
  var workerMessage = {
    workerID: "",
    MAX_ITERATIONS: MAX_ITERATIONS,
    ReMax: ReMax,
    ReMin: ReMin,
    ImMax: ImMax,
    ImMin: ImMin,
    grayscaleFactor: grayscaleFactor,
    canvasWidth: canvasWidth,
    canvasHeight: canvasHeight,
    imageDataObject: imageDataObject
  };
  
  function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
    if (fineDetailMandelbrotReceived) {
      return; // For some reason, the fine detail callback finished before the coarse detail callback. Don't display the coarse Mandelbrot image.
    }
    
    ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
    globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
    globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          
  
    var elapsedMilliseconds = (new Date()) - startTime;
    elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.iterationSum.format() + " iterations in " + (elapsedMilliseconds / 1000).toFixed(2) + " seconds"; // Note that the UI element is not updated until after this block terminates (which is the desired behavior).            
    
    if (evt.data.workerID == "Fine detail") {
      fineDetailMandelbrotReceived = true;
      messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
    } // if
  } // workerCallback
  
  globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
  globals.fineDetailWorker.onmessage = workerCallback;

  workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always been a (positive) integer.
  workerMessage.workerID = "Coarse detail";
  globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

  workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
  workerMessage.workerID = "Fine detail";                
  globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.
} // drawMandelbrot

지금은 workerCallback 콜백 함수를 건너뛰고 다음으로 drawMandelbrot 함수에서 다음 줄을 실행합니다.


globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
globals.fineDetailWorker.onmessage = workerCallback;

workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always be a (positive) integer.
workerMessage.workerID = "Coarse detail";
globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
workerMessage.workerID = "Fine detail";                
globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.

즉, 두 작업자 개체 모두에 대해 작업자 스레드가 UI 스레드에 개체 리터럴 형식으로 메시지를 반환할 때 실행할 콜백 함수(workerCallback)를 지정합니다.

다음으로 나중에 어떤 개체 리터럴이 어떤 작업자에서 생성되었는지 확인하려면 빈 workerID 값을 설정합니다.

마지막으로 이전에 생성된 workerMessage 개체 리터럴 메시지를 작업자 스레드에 게시하여 처리합니다. 다음은 (DOM 비인식) 작업자 스레드 코드에 수신되는 두 개의 메시지입니다.

mandelbrotWebWorker.js


/* 
  Recall that a Web Worker JavaScript file has no access to the DOM and, in general, cannot receive DOM related objects.  
*/

self.onmessage = function(evt) {
  var MAX_ITERATIONS = evt.data.MAX_ITERATIONS;
  var ReMax = evt.data.ReMax;
  var ReMin = evt.data.ReMin;
  var ImMax = evt.data.ImMax;
  var ImMin = evt.data.ImMin;
  var grayscaleFactor = evt.data.grayscaleFactor;
  var canvasWidth = evt.data.canvasWidth;
  var canvasHeight = evt.data.canvasHeight;
  var imageDataObject = evt.data.imageDataObject; // This is a copy of, not a reference to, globals.canvas.context.imageDataObject.
  
  var imageDataObjectData = imageDataObject.data; // As a performance optimization, we cache this data array.
  var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
  
  var x_coefficient = (ReMax - ReMin) / canvasWidth; // Keep the below loops as computation-free as possible.
  var y_coefficient = (ImMin - ImMax) / canvasHeight; // Keep the below loops as computation-free as possible.
  
  var iterationSum = 0;
  var currentPixel = 0;  
  for (var y = 0; y < canvasHeight; y++) {
    var c_Im = (y * y_coefficient) + ImMax; // Note that c = c_Re + c_Im*i
    
    for (var x = 0; x < canvasWidth; x++) {
      var c_Re = (x * x_coefficient) + ReMin // Convert the canvas x-coordinate to a complex plane Re-coordinate. c_Re represents the real part of a c value.
      
      var z_Re = 0; // The first z value (Zo) must be 0.
      var z_Im = 0; // The first z value (Zo) must be 0. Note that z = z_Re + z_Im*i
      
      var c_belongsToMandelbrotSet = true; // Assume that the c associated with Zn belongs to the Mandelbrot set (i.e., Zn remains bounded under iteration of Zn+1 = (Zn)^2 + c).
      var exponentialSmoothingSum = 0; // Used to color the c-value pixels that are not part of the Mandelbrot set (i.e., tend toward infinity under iteration of Zn+1 = (Zn)^2 + c).
      for (var iterationCount = 1; iterationCount <= MAX_ITERATIONS; iterationCount++) {
        iterationSum++; // Keep track of how many iterations were performed in total so we can report this to the user.
      
        var z_Re_squared = z_Re * z_Re; // A small speed optimization.
        var z_Im_squared = z_Im * z_Im; // A small speed optimization.
                      
        exponentialSmoothingSum += Math.exp( -(z_Re_squared + z_Im_squared) ); // Technically, this should be e^(-|z|). However, avoiding the expensive square root operation does not significantly effect the resulting image.              
        if (exponentialSmoothingSum >= 255) { // Don't cycle through the (gray) colors.
          exponentialSmoothingSum = 255;
        } // if

        if (z_Re_squared + z_Im_squared > 4) { // Checks if |z^2| is greater than 2. This approach avoids the expensive square root operation.
          c_belongsToMandelbrotSet = false; // This complex c value is not part of the Mandelbrot set (because it will always tend towards infinity under iteration).
          break; // Immediately check the next c value to see if it belongs to the Mandelbrot set or not.
        } // if
        
        // The next two lines perform Zn+1 = (Zn)^2 + c (recall that (x + yi)^2 = x^2 - y^2 + 2xyi, thus the real part is x^2 - y^2 and the imaginary part is 2xyi).
        z_Im = (2 * z_Re * z_Im) + c_Im; // We must calculate the next value of z_Im first because it depends on the current value of z_Re (not the next value of z_Re).
        z_Re = z_Re_squared - z_Im_squared + c_Re; // Calculate the next value of z_Re.
      } // for   
      
      if (c_belongsToMandelbrotSet) { // This complex c value is probably part of the Mandelbrot set because Zn did not tend toward infinity within MAX_ITERATIONS iterations.
        imageDataObjectData[currentPixel++] = 0; // Red. Note that there are 255 possible shades of red, green, blue, and alpha (i.e., opacity).
        imageDataObjectData[currentPixel++] = 0; // Green.
        imageDataObjectData[currentPixel++] = 0; // Blue.
        imageDataObjectData[currentPixel++] = 255; // Alpha (i.e., 0% transparency).
      } 
      else { // This complex c valus is definitely not part of the Mandelbrot set because Zn would tend toward infinity under iteration (i.e., |Zn| > 2).
        var pixelGrayscaleValue = 255 - exponentialSmoothingSum % 256; // Force the value of exponentialSmoothingSum to be between 0 and 255 inclusively. Note that all values for red, green, and blue are identical when using a grayscale.
        var adjustedPixelGrayscaleValue = pixelGrayscaleValue * grayscaleFactor;
        
        imageDataObjectData[currentPixel++] = adjustedPixelGrayscaleValue; // Because we mod by 256, the value of exponentialSmoothingSum will always be between 0 and 255.
        imageDataObjectData[currentPixel++] = adjustedPixelGrayscaleValue; // If exponentialSmoothingSum is 255 (it's maximum possible value), then 255 % 256 = 255.
        imageDataObjectData[currentPixel++] = adjustedPixelGrayscaleValue; // When exponentialSmoothingSum is 255, we have 255 - 255 = 0, so the shade values for RGB are all set to 0 (that is, the c-value pixel is rendered black - indicating that this particular c-value very slowly tends towards infinity).
        imageDataObjectData[currentPixel++] = 255; // Always draw the c-value pixels with no transparency.
        
        if (pixelGrayscaleValue > maxPixelGrayscaleValue) {
          maxPixelGrayscaleValue = pixelGrayscaleValue; // Determine the lightest shade of gray in case the user clicks the Lighten button.
        } // if
      } // if-else
    } // for
  } // for  
  
  self.postMessage({
    imageDataObject: imageDataObject,  
    maxPixelGrayscaleValue: maxPixelGrayscaleValue,
    workerID: evt.data.workerID,
    iterationSum: iterationSum
  });
} // self.onmessage

작업자 스레드 코드가 단일 비동기 함수 내에 포함되어 있습니다.


self.onmessage = function(evt) { ... }

이 비동기 함수는 메시지가 UI 스레드에서 전송될 때 호출됩니다. 전송된 메시지는 evt.data 내에 포함됩니다. 성능을 향상시키기 위해 메시지 내에 포함된 데이터가 작업자 스레드 로컬 변수로 전송됩니다.


var MAX_ITERATIONS = evt.data.MAX_ITERATIONS;
var ReMax = evt.data.ReMax;
var ReMin = evt.data.ReMin;
var ImMax = evt.data.ImMax;
var ImMin = evt.data.ImMin;
var grayscaleFactor = evt.data.grayscaleFactor;
var canvasWidth = evt.data.canvasWidth;
var canvasHeight = evt.data.canvasHeight;
var imageDataObject = evt.data.imageDataObject; // This is a copy of, not a reference to, globals.canvas.context.imageDataObject.

Mandelbrot 이미지 계산 코드는 이미지 데이터 배열을 캔버스에 직접 그리는 대신 호출 UI 스레드(UI 스레드에서 처리할 몇 가지 다른 항목과 함께)로 다시 전달한다는 점을 제외하면 정확히 이전과 같습니다.


self.postMessage({
  imageDataObject: imageDataObject,  
  maxPixelGrayscaleValue: maxPixelGrayscaleValue,
  workerID: evt.data.workerID,
  iterationSum: iterationSum
});

self.postMessage 호출에서는 다음과 같은 콜백을 UI 스레드에서 호출하여 개체 리터럴을 전달합니다.


function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
  if (fineDetailMandelbrotReceived) {
    return; // For some reason, the fine detail callback finished before the coarse detail callback. Don't display the coarse Mandelbrot image.
  }
  
  ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
  globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
  globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          

  var elapsedMilliseconds = (new Date()) - startTime;
  elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.iterationSum.format() + " iterations in " + (elapsedMilliseconds / 1000).toFixed(2) + " seconds"; // Note that the UI element is not updated until after this block terminates (which is the desired behavior).            
  
  if (evt.data.workerID == "Fine detail") {
    fineDetailMandelbrotReceived = true;
    messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
  } // if
} // workerCallback


즉, 작업이 완료되면 작업자 스레드에서 workerCallback 함수를 호출합니다. 작업자 스레드의 개체 리터럴은 연관된 이벤트 개체(evt.data)에 포함되어 있습니다. 작업자 스레드에서 UI 스레드로 전달된 네 가지 정보는 다음과 같습니다.

  • evt.data.imageDataObject - 완료된 Mandelbrot 이미지를 캔버스 데이터 배열 형식으로 포함합니다. 그런 다음 ctx.putImageData(evt.data.imageDataObject, 0, 0)에서 캔버스(UI 스레드)에 이미지를 그립니다.
  • evt.data.maxPixelGrayscaleValue - 사용자가 밝게 단추를 클릭하는 경우에 일반적으로 필요한 가장 밝은 회색 음영을 Mandelbrot 이미지에 포함합니다.
  • workerID - 개체 리터럴이 생성된 작업자 스레드를 나타냅니다. 정교한(높은 상세) 작업자 스레드가 과정(낮은 상세) 작업자 스레드보다 먼저 완료될 수 있는 경우를 파악해야 합니다.
    
    if (fineDetailMandelbrotReceived) {
      return; // For some reason, the fine detail callback finished before the coarse detail callback. Don't display the coarse Mandelbrot image.
    }
    
    
    
  • iterationSum - 요청한 Mandelbrot 이미지를 생성하는 데 필요한 반복 횟수를 제공합니다. 이 값은 Fine detail 작업자 스레드에서 Mandelbrot 이미지를 계산하는 데 필요한 시간(초)과 함께 Mandelbrot 8 응용 프로그램에 대한 성능 메트릭을 제공합니다.

작업자 스레드의 사용 가능성은 성능 향상을 위해 가능한 많은 알고리즘을 열어 줍니다. 예를 들어 빈 화면이 길어지지 않도록 하려면 낮은 상세 및 높은 상세 이미지를 동시에 계산하는 대신 두 개의 작업자 스레드를 사용하여 높은 상세 Mandelbrot 이미지의 위쪽 절반과 아래쪽 절반을 동시에 계산할 수 있습니다. 이 방법은 설명하지 않고 Mandelbrot 9(소스 코드를 보려면 페이지를 마우스 오른쪽 단추로 클릭)에서 보여 주며 연관된 JavaScript 파일은 mandelbrotWebWorkerManager.jsmandelbrotWebWorker.js입니다.

마지막 Mandelbrot 탐색기 섹션에서는 모든 모서리에서 시작하여 확대/축소 상자를 그릴 수 있도록 확대/축소 상자 코드를 개선합니다.

관련 항목

Mandelbrot 탐색기

 

 

표시:
© 2014 Microsoft