Jump to content
warleyalex

Black Screen of Death

Recommended Posts

We've converted more than 64K source lines code to SMS in 24h, the code was structured in various units (32). Most of the EWB code translates well, but more unusual code and some sophisticated language feature (e.g. property overloading), we've got crashed with runtime errors, internal errors, but a few patches got gents to about 90% of what we needed. Even then, the remaining 10% was till enormous. 

When running the project, I can't see anything except a black screen, no messages errors, we usually get some obscure hidden error occurred somewhere. Yes, this is a real nightmare. The issue is probably on the big uWebUI unit (3x bigger then SmartCL.Components ), it has more than 15K of source lines.

A very challenging is discover where lives the source of the black screen.

Share this post


Link to post
Share on other sites

When you start you app and get the black screen, click on DevTools to open the internal debugger:

image.png

Then click Reload to reload the page. Click on that red X in the upper right corner:

image.png

Then expand the error message to get the stack trace::

image.png

This was an example where I introduced a bug in InitializeForm.

You can also try using breakpoints by clicking on that "bug" icon:

image.png

Then you should be able to set breakpoints in your code. Unfortunately I did not get it to stop during initialization, so it may be that this is not working properly during app startup.

And the last thing is to use old-school WriteLn like it has been suggested earlier. However, that may not work if your bug only happens on mobile devices where you don't have a debugger. In that case you can use Application.DebugPrint:

  1. Set Application.OnDebugPrint as early as possible to point to your own procedure
  2. In this procedure collect the debug information.
  3. Add SmartCL.Application and SmartCL.Debug to uses clauses
  4. In any unit file: Application.DebugPrint('Your message');
  5. Show the debug information where it's comfortable for you

Good luck with your bug hunt!

Share this post


Link to post
Share on other sites

When I start the app and I get the black screen hell, no syntax errors, no hints. Logic errors are more difficult to debug than syntax errors because they're not something that outwardly looks wrong. The syntax of the code is written correctly, but it's just not doing what you want it to do. Debugging logic errors requires more work, luckily I could find the issue in minutes. The uWebUI unit have 15,550 lines, and apparently there are some logic error in some place that I'm getting the black screen,  the JavaScript Debugger tool makes the work much easier to find syntax error, but where to set the breakpoint?

Before I begin debugging, let's take a look at the problem: In the uWebUI unit, the main class is TInterfaceManager. We know that "the TInterfaceManager class represents the interface manager, which is responsible for managing the user interface of an application at both design-time and run-time.  A global instance of the TInterfaceManager class is created automatically at application startup, and is called InterfaceManager".

we have to invoke the debugger at the TInterfaceManager constructor!

Firstly, I've created a global function

function debugger: variant; external "debugger";

constructor TInterfaceManager.Create;
begin
   inherited Create;

  debugger;

{...}

end;

Stepping Through Code, using F9 and F10, I couldn't find the error.

  • Step Over (F9)—Step Over allows you to execute one line of code at a time. When you're on a line that executes an if statement and you press Step Over, it checks to see whether the condition in the if statement is met. If the condition is met, the debugger moves to the next line. When the condition in the if statement is not met, the debugger jumps to the else portion of the statement (assuming that there is an else portion) or jumps to the next section of code.

  • Step Into (F10)—Step Into allows you to step into a function and monitor the execution of each line in that function. Once inside the function, you can use Step Over to move line by line.

  • Step Out (F11)—Use Step Out when you have stepped into a function, have verified what you wanted to see, and want to automatically execute the rest of the function without having to step over each remaining line of the function. Once the Debugger completes running through the function, you can continue stepping through the rest of the code line by line (if you want).

I could Identify a suspect method called CreateRootElement(ROOT_ELEMENT_NAME,ELEMENT_CLASS_BODY); 

I don't know, I've been thinking, is this method trying to create the body element? Just commented this method and no more black screen!

Thank you a lot.

 

Share this post


Link to post
Share on other sites

One thing that can be a bit of a challenge when coming from Delphi or FPC, is that Javascript is 100% ASYNC by nature. This means that from the time that you call the constructor, to the time that the element has been successfully created, styled and injected into the DOM - 1000 other things can happen in between. This is true for everything, even loading third party libraries, css files, images; everything is async.

There is no such method in our core RTL that you describe (CreateRootElement), this is a part of Chrome itself. So unless you are using a third party library like jquery or something that doesnt synchronize with our RTL - this is too little to go on to properly help you. But when you run your program, click the buttons in the following order, check the "pause on caught exception" (note: there is a deliberate exception when the program starts checking for a feature, but this is handled so just click the blue arrow to continue). Below you see how the callstack looks for an exception i intentially created. From the event being issued, delegated, dispatched and handled.

image.png

 

 But i doubt this is where your problem stems from. I think this has to do with synchronization and that you are expecting code to execute linear. Hence your code crash because whatever you are trying to access is either not finished yet, fully loaded or in the process or being created.

It is a typical thing to experience when coming from Delphi or FPC, because you are used to linear execution. Here are a few pointers on how to synchronize your code and what to override. Because there are 3 substancial differences (InitializeObject, FinalizeObject and ObjectReady. More about those as we move along).

When you first look at examples etc. you will notice calls like TW3Dispatch.Execute() which is often used to create small timed delays. A bit like TTimer in Delphi but wrapped into a single method. The same class, TW3Dispatch also has other handy methods. You will find it in System.Time.pas and SmartCL.Time.pas. Smart supports partial classes, which means the content of classes can be finished and/or added to in different units (much like C#, C++ and Java has; with emphasis on Mono C#). This is very effective when building multi target frameworks.

The second thing to explore is the use of callbacks when writing code that should run in a purely async environment. This can take some getting used to, but once its in your fingers you will wonder how you ever lived without it (or at least parts of it). So learning to properly synchronize things is the key here. This is why you never override the constructor in Smart, you override InitializeObject() and FinalizeObject() instead which ensures a level of synchronized execution.

For example, let's say you are loading a file right? Since JS is async that means whatever Load() routine you use (even direct access to the filesystem under node.js) will finish and exit before any data is available. This can be bewildering to say the least. Until you start to make some observations on how it's best handled. Loading files can be done in many ways, but for browser apps -using the methods in TW3Storage is the easiest (in SmartCL.FileUtils.pas).

Here is an example of how you could use callbacks in your own classes to better deal with async IO:

// First lets define a callback. Just like events they dont need "of object" postfix
// or "reference to" prefixing. Its fairly straight forward stuff.
// Note: The procedure we define is actually "TStdCallback" in System.Types.pas

type
  TMyDataClassReadyCB = procedure(Success: boolean);

// OK now lets imagine we have a class for loading some text data. It takes text and builds
// an object model from this. If the object model is loaded and valid then Resize() will
// do layout and show the data. So let's define a loading method that takes our
// callback as a parameter:

procedure TMyDataClass.LoadDataFromURI(const URI: string; const CB: TMyDataClassReadyCB);
begin
  // We let TW3Storage do the dirtywork. Note that there are several overloaded
  // methods for loading data in the class. This one loads any file and 
  // returns it as text, but you can also get the data as a TStream (have a peek at the class)
  TW3Storage.LoadFile(URI, procedure (const FromUrl: string;
  	const TextData: string; const Success: boolean)
  begin
   // Loaded ok? Then keep the data
   if Success then
       SetRawData(TextData);
 
   // Regardless of outcome, always call back!
   if assigned(CB) then
     CB(Success);
  end);
end;

// When calling the method from the UI, say a button-click, we can be more
// flexible with how we handle control states. The callback will fire
// regardless of success or failure
procedure TForm1.BtnLoadDataOnClick(Sender: TObject)
begin
  // Disable load button and listbox while we are loading.
  // Also set the cursor to a progress animation
  BtnLoadData.enabled := false;
  lstDataView.enabled := false;
  Cursor := crProgress;

  // Now use our loading method
  FMyData.LoadDataFromURI('res/defaultdata.txt', procedure (Success: boolean)
  begin
    // Loading is done! Regardless if it failed or not we adjust
    // the UI's state back to how it was. With one exception,
    // namely that we let our list remain disabled if no data is present
    Cursor := crDefault;
    BtnLoadData.enabled := true;
    lstDataView.enabled := Success;
    
    // Use an inline variable to remember if our list
    // held any data and was cleared
    var HadVisibleData := lstDataView.Items.Count > 0;
    
    // OK let's build a data-model if we got some data to work with.
    // If the load failed then clear the list. If the load failed the list
    // has already been disabled (see above) so we just need a clear() call.
    // Javascript is laid back, i prefer to be more strict.
    lstDataView.BeginUpdate();
    try
      // Flush our list if it had content
      if HadVisibleData then
        lstDataView.Items.Clear();
      
	  // Any data loaded? Build an object model from it
      if Success then
        buildDataModel();
    finally
      // Mark the list as needing a Resize() call if we either
      // cleared old data, loaded new data -or both!
      if HadVisibleData or Success then	
      	lstDataView.AddToComponentState([csResizing]);
      
      // End update, it's all good
      lstDataView.EndUpdate();
    end;
  end);
end;

One of the root classes for visual controls, TW3TagContainer, has a method you want to investigate. It will save you a lot of time:

    // Does a check if the child elements are in ready-state
    function  QueryChildrenReady: boolean; virtual;

This method will recursively check that all child controls have been created and have their csReady flag set in the ComponentState value-set. I find this to be very handy when writing large and complex visual controls where you need to know 100% that children are ready before you access them. The way JS works is that even though the constructor is finished and the element created --the prototype can still be under construction in the background. It is very rare to encounter this in our new RTL, but it can occur if you expect controls to be ready after a call to Create().

There is also a method to synchronize resize calls. This is quite handy to add in the ObjectReady() method. All visual elements have an ObjectReady() method that is called after the element has been injected into the DOM. This executes just between the time when an element is finished, but before it's visually presented. If you want an immediate resize when all child controls are created and ready, calling ResizeWhenReady() is the ticket.

procedure TW3MovableControl.ResizeWhenReady;

Next there is type helpers. Handles for example come in many shapes, and most of them (like THandle, TControlHandle, TGradientHandle, TCSSStyleHandle) have a helper class with low-level methods. Sadly these are not always shown in the code suggestion dialog, but that will be improved on in future updates.

Now TControlHandle has a method called ReadyExecute(), and as the name implies this sets a callback that fires when the element has been created and been injected into the DOM. But this is low-level so we are not talking about the whole object pascal custom-control, but rather the raw HTML element that the control manages. It should be noted that overriding ObjectReady() is the proper way to do things, but sometimes you have to "bend the rules".

While a bit esoteric looking calling a method on a handle, this is perfectly valid:

procedure TSomeControl.InitializeObject;
begin
  inherited;
  ...
  Handle.ReadyExecute( procedure ()
  begin
    // Do something at once when the element is injected into the DOM.
  end);
end;

I think these will help you synchronize your code when porting from Delphi or FPC. In 99% of cases where people experience problems with porting over code, it comes down to synchronization. The JavaScript Virtual Machine simply doesnt follow conventional behavior and proper use of callback handlers, events and delayed execution is the way things work. The bonus is that you are not boxed into a "fake" linear environment (which could be achieved), communicating with JS libraries and mapping directly to existing JS objects is 1:1 rather than resorting to elaborate interface files.

Paradoxical as it may sound, with correct use you can easily produce code that runs faster or en-par with the tools you are used to.  We have node.js code in our lab that actually out-performs native, monolithic code. When you add clustering and domain level programming to that - the possebilities are near endless.

Have a peek at this demo for examplehttp://quartexhq.myasustor.com/applications/particles/

We use TW3Dispatch.RequestAnimationFrame() to ensure a steady 60fps rendering, but it runs fine at 120fps on relatively modest PC specs. So its a matter of going with the flow and learn the subtle differences.

Share this post


Link to post
Share on other sites
18 hours ago, EWB said:

where to set the breakpoint?

Your profile says that you're using the Basic Edition. Only the Professional and Enterprise versions have the intergrated debugger, where you can set breakpoints just like in Delphi.

Share this post


Link to post
Share on other sites

Just place DebugBreak somewhere in your code, and it will act like a breakpoint. 

You need to have Chrome Developer Tools open for this to work (hit F12). If you have Developer Tools open, an extra bit of awesomeness is that you can click and hold the Refresh button to clear the cache. I step through Code using chrome directly not using the internal SMS integrated debugger!  

procedure TForm1.Button1Click(Sender: TObject);
begin
  DebugBreak;
  console.log('Button1 clicked');
end;

My SMS integrated debugger is disabled. 

 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×