(3 de 5) JavaScript - Break Your Code With The Debugger Statement - DZone Performance
(3 de 5) JavaScript - Break Your Code With The Debugger Statement - DZone Performance
Statement
by Raghuraman Kesavan · Aug. 23, 17 · Performance Zone · Tutorial
xMatters delivers integration-driven collaboration that relays data between systems, while engaging the
right people to proactively resolve issues. Read the Monitoring in a Connected Enterprise
whitepaper and learn about 3 tools for resolving incidents quickly.
In this post, we will learn more about the debugger statement available in the JavaScript library. Many
JavaScript developers may not aware of the debugger statement. I generally believe that most JavaScript
developers use any of the below four methods for client-side debugging:
console.log() Statement
Setting breakpoint using developer tools available in the browser (preferred)
Debugger Statement
alert statement (mostly beginners will use this)
Here, we are primarily going to talk about the debugger statement. Before that, the console object provides
access to the browser's debugging console and has many inbuilt methods available with it. Some of the
example methods are
1 console.trace();
1 console.warn();
1 console.log();
1 console.info();
1 console.error();
1 console.clear();
You can use these methods to differentiate the console message. Check this URL to learn more about console
objects.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 1/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
Debugger Statement
The debugger statement invokes any available debugging functionality, such as setting a breakpoint. If no
debugging functionality is available, this statement has no effect.
Syntax:
debugger;
Code:
1 function potentiallyBuggyCodeAvailable(){
2 code;
3 debugger;
4 }
When the debugger is invoked, execution is paused at the debugger statement. It is like a breakpoint in the
script source.
In the above code, when the submit button is clicked it, will call sum() method and the code will break where
the debugger statement is added.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 2/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
In the above screenshot, the code was broken where the debugger statement is added. It will be really helpful
under certain circumstances.
If you enjoyed this article, please share it with your developer friends on social media.
Discovering, responding to, and resolving incidents is a complex endeavor. Read this narrative to learn
how you can do it quickly and effectively by connecting AppDynamics, Moogsoft and xMatters to create
a monitoring toolchain.
11 Debugging Tips That Will Save You Time Debug JavaScript in Google Chrome’s Dev
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 3/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
Coverage Analysis
by Sebastian Paulsen · Sep 24, 18 · Performance Zone · Tutorial
Coco helps by displaying the function coverage of the execution of the program. In this tip of the week, I
would like to show you the perks of using Coco's Function Coverage for inding unused functions in your code
project.
1 #include <stdio.h>
2 #include <string.h>
3
4 int A(char *arg)
5 {
6 printf("A\n");
8 }
9
10 int B(char *arg)
11 {
12 printf("B\n");
14 }
15
16 int C(char *arg)
Complete Guide to Preventing ATO
17 {
• Measure the Impact of Account Takeover Download Free PDF
18 printf("C\n"); • Learn How to Spot Account Takeover Attempts
19 return !strcmp(arg, "TRUE");
• Implement Smart Account Takeover Prevention
20 }
21
22 int D(char *arg)
23 {
24 printf("D\n");
26 }
27
28
29 int main(int argc, char **argv)
30 {
32 printf("TRUE\n");
33 } else {
34 printf("FALSE\n");
35 }
36 return 0;
37 }
We will irst instrument the code with Coco's Coveragescanner tool. The result is an instrumented executable,
and also a .csmes ile which contains a database of code lines and their source locations.
Next, we run the instrumented program with speci ic arguments to cause it to execute a code path. This will
create an execution report (.csexe ile) which contains information about which code was executed. This ile
will be imported into the generated (.csmes ile). As it turns out, with these arguments, the functions B and D
are not hit by the test case.
We can import the .csexe ile into the .csmes ile with the following command.
The inal step is to generate the report for the unused functions. Hereby we use the option --function-coverage
to specify the coverage level. '--format-unexecuted=' prints the unused functions into the ile result.txt by
irst writing the ile( '%f') and then then line ('%l').
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 5/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
2 .../example.c 22
If we want to analyze more runs of the program, we can just run the ./prog command with different
arguments. The executions will result in additional data concatenated to the .csexe ile.
For further information about cmreport and other features of Coco, follow this link.
on OpenCoverage
Topics: CODE COVERAGE, C, C++, FUNCTIONS, PERFORMANCE, CLEAN CODE, TUTORIAL, CODE ANALYSIS
Published at DZone with permission of Sebastian Paulsen . See the original article here.
Opinions expressed by DZone contributors are their own.
to Gandiva
by Ravindra Pindikura · Sep 24, 18 · Performance Zone · Tutorial
xMatters delivers integration-driven collaboration that relays data between systems, while engaging the
right people to proactively resolve issues. Read the Monitoring in a Connected Enterprise
whitepaper and learn about 3 tools for resolving incidents quickly.
You’re probably already aware of the recently announced Gandiva Initiative for Apache Arrow, but for those
who need a refresher, this is the new execution kernel for Arrow that is based on LLVM. Gandiva provides
very signi icant performance improvements for low-level operations on Arrow buffers. We irst included this
work in Dremio to improve the ef iciency and performance of analytical workloads on our platform, which
will become available to users later this year. In this post we will provide a brief overview for how you would
develop a simple function in Gandiva as well as how to submit it to the Arrow community.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 6/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
p p y
Fundamentally, Gandiva uses LLVM to do just-in-time compilation of expressions. The dynamic part of the
LLVM IR code is generated using an IRBuilder, and the static part is generated at compile time using clang. At
Complete Guide to Preventing ATO
run-time, both the parts are combined into a single module and optimized together. For most new UDFs,
• Measure the Impact of Account Takeover Download Free PDF
adding a hook in the static IR generation technique is suf icient. More details about Gandiva layering and
• Learn How to Spot Account Takeover Attempts
optimizations can be found here. • Implement Smart Account Takeover Prevention
Function Categories
The functions supported in Gandiva are classi ied into one of three categories based on how they treat null
values. Gandiva uses this information during code-generation to reduce branch instructions, and thereby,
increasing CPU pipelining. The three categories are as follows:
1. NULL_IF_NULL Category
In this category, the result of the function is null if and only if one or more of the input parameters are null.
Most arithmetic and logical functions come under this category.
For these functions, the Gandiva layer does all of the null handling. The actual function de inition can simply
ignore nulls.
For example:
1 int32
4 }
2. NULL_NEVER Category
In this category, the result of the function cannot be null i.e the result is non-nullable. But, the result value
depends on the validity of the input parameters. The function prototype includes both the value and the
validity of each input parameter.
1 bool
3 If (!is_first_valid || !is_second_valid) {
4 return false;
5 }
7 }
3. NULL_INTERNAL Category
In this category, the result of the function may be null based on some internal logic that depends on the value
of the internal values/validity. The function prototype includes both the value and the validity of each input
parameter, and a pointer for the result validity (bool).
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 7/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
1 int32
7 }
9 is_result_valid = true;
10 return int_value;
11 }
Clone the Gandiva git repository and build it on your test machine or desktop. Please follow the instructions
here.
We will add our simple function to the existing arithmetic_ops.cc. For more complex functions or types it’s
better to add to a new ile.
1 FORCE_INLINE
4 }
The function registry includes the details of all supported functions in Gandiva. Add this line to the
pc_registry_ array in function_registry.cc
Complete
4. Function category NULL_IF_NULL Guide to Preventing ATO
• Measure the Impact of Account Takeover
5. Implemented in function my_average_int32_int32 Download Free PDF
• Learn How to Spot Account Takeover Attempts
• Implement Smart Account Takeover Prevention
4. Add Unit Tests
For this simple function, we will skip adding a unit test. For complex functions, it’s recommended to add a
unit test for the new function.
We’ll add an integ test by building a projector in projector_test.cc that computes the average of two columns.
1 TEST_F(TestProjector, TestIntMyAverage) {
2
3 // schema for input fields
7
8 // output fields
10
11 // Build expression
auto avg_expr = TreeExprBuilder::MakeExpression("my_average", {field0, field1}, field_avg);
12
13 std::shared_ptr<Projector> projector;
15 EXPECT_TRUE(status.ok());
16
17 // Create a row-batch with some sample data
18 int num_records = 4;
20 auto array1 = MakeArrowArrayInt32({11, 13, 15, 17}, {true, true, false, true});
21
22 // expected output
24
25 // prepare input record batch
27
28 // Evaluate expression
29 arrow::ArrayVector outputs;
31 EXPECT_TRUE(status.ok());
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 9/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
32
33 // Validate results*
34 EXPECT_ARROW_ARRAY_EQUALS(exp_avg, outputs.at(0));
Complete Guide to Preventing ATO
35
}
• Measure the Impact of Account Takeover Download Free PDF
36
• Learn How to Spot Account Takeover Attempts
• Implement Smart Account Takeover Prevention
5. Build Gandiva
1 $ cd <build-directory>
2 $ make projector_test_Gandiva_shared
8
9 $ ./integ/projector_test_Gandiva_shared
15 <SNIPPED>
16 [ RUN ] TestProjector.TestIntMyAverage
21 [ PASSED ] 11 tests.
First, you must push the changes to your fork and create a PR against an upstream project. This is best done
by pushing to your local repo and raising a PR request on the Gandiva page using the diff with your repo.
Once the PR is created, the community can review the code changes and merge.
The full code listing for this simple function is present in this PR — it includes both a projector test and a
ilter test.
Open the ir.txt ile and search for "myaverage." You should see the IR code (unoptimized)
5 ret i32 %4
6 }
First, modify the optimizer function to dump the IR code. The easiest way to do this is by modifying this line
(move the call to DumpIR to outside the if condition.)
1 $ cd <build-directory>
2 $ make projector_test_Gandiva_shared
8 $ ./integ/projector_test_Gandiva_shared
You should see the optimized IR code on stdout. You’ll notice that the:
As this runs the vectorized snippet shows the function processing four integers at a time: adds 4 integers to 4
integers, divides all the 4 integers by 2, and so on…
1 vector.body:
2 ..
5 %18 = sdiv <4 x i32> %16, <i32 2, i32 2, i32 2, i32 2>
6 %19 = sdiv <4 x i32> %17, <i32 2, i32 2, i32 2, i32 2>
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 11/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
7 ..
8 store <4 x i32> %18, <4 x i32>* %21, align 4, !alias.scope !8, !noalias !10
9 store <4 x i32> %19, <4 x i32>* %23, align 4, !alias.scope !8, !noalias !10
%index.next = add i32 %index, 8
Complete Guide to Preventing ATO
10
• Measure the Impact of Account Takeover Download Free PDF
In this article, we gave an overview of adding a simple function to Gandiva. In subsequent articles, we’ll
• Learn How to Spot Account Takeover Attempts
extend this to functions for other categories and functions using libraries from c++ std or boost.
• Implement Smart Account Takeover Prevention
We also have more features coming that deal with supporting pluggable function repositories and more
optimizations (eg. special handling for batches that have no nulls.) More to follow!
Discovering, responding to, and resolving incidents is a complex endeavor. Read this narrative to learn
how you can do it quickly and effectively by connecting AppDynamics, Moogsoft and xMatters to create
a monitoring toolchain.
Fixing an Apache Tomcat Installation Error Setting Up New Relic on Django, Apache
Published at DZone with permission of Ravindra Pindikura . See the original article here.
Opinions expressed by DZone contributors are their own.
Application performance monitoring (APM) is a section of IT that ensures applications are performing as
expected. Application monitoring tools maintain this monitoring. The ultimate goal of performance
monitoring is to supply end users with a top quality end-user experience.
Application monitoring tools give administrators the information they need to quickly igure out issues that
negatively impact an application’s performance. Such tools can be speci ic to a selected application or
monitor multiple applications on a constant network, grouping data concerning client CPU usage, memory
demands, data output, and overall bandwidth.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 12/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
Application performance management is basically a term for anything to do with managing or monitoring the
performance of your code, application dependencies, and user experience.
Complete Guide to Preventing ATO
• Measure the Impact of Account Takeover Download Free PDF
Varieties of Application Performance
• Learn Monitoring
How to Spot Account Tools
Takeover Attempts
• Implement Smart Account Takeover Prevention
1. App metrics based: Many tools use numerous server and app metrics and call it APM. At best, they will
tell you how many requests your app gets and potentially which URLs can be slow. Since they don’t do
code level pro iling, they can’t tell you why.
2. Code-level performance based: The standard type of application performance management products
based on code pro iling and transaction tracing.
3. Network-based: These tools measure application performance as per the network traf ic. There is an
entire product class known as NPM that focuses on this kind of solution.
At the center of APM, you need to be able to measure the performance of each request and transaction in your
application. You’ll use this to see which requests are accessed the most, which are the slowest, and which you
should increase your backlog to boost. Knowing the performance of each request is just the beginning,
though. You could get that from a server access log. The real secret is understanding why.
The reason your application is slow typically comes down to a spike in traf ic or a tangle with one of your
application dependencies. It is common to have these kinds of problems when
If you wish to know why your application is slow, throwing errors, or has bugs, you’ve got to get right down
to the code level. Knowing that an explicit request doesn’t work is vital and pretty straightforward. Deciding
why it doesn’t work is tough, and generally very exhausting.
By tracking what your application is doing with an application performance monitor all the way down to the
code level, you will gain far more insights on what’s occurring, such as
Whenever anything goes wrong in production, the irst thing you’ll hear a developer say is "send me the logs."
Log information is typically the eyes and ears of developers once their applications are deployed. Developers
want access to their logs via a centralized logging solution like a log management product. Luckily, log
management is an APM feature included in Retrace. Most APM solutions don’t support the #1 factor
developers wish to see…their logs!
Application Errors
The last thing we need is for a user to contact us and tell us that our application is giving them an error or
simply blowing up. As developers, we have to bear in mind that this happens and we are perpetually
anticipating them. Errors are the primary line of defense for locating application issues. We want to ind and
ix the errors, or at least understand them, before our customers call to inform us — most of them won’t even
call to inform you. They’ll simply go elsewhere.
Excellent error tracking, reporting, and alerting are essential to developers in an application performance
management system. We would suggest ixing alerts for brand new exceptions as well as monitoring overall
error rates. Any time you are preparing for production, you should be watching your error dashboards to see
if any new issues have arisen. Odds are, you’ll realize some sort of new error that you will then quickly hot ix.
Understand the performance of your applications on the server aspect is very important. However, today’s
applications use JavaScript and it’s vital to monitor how long it takes their browser to completely load and
render your web content. A straightforward JavaScript error or slow loading ile might utterly dis igure your
application. Real user monitoring, or RUM, is another vital feature of APM that developers use to monitor
their applications.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 14/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
Published at DZone with permission of Amit Shingala . See the original article here.
Opinions expressed by DZone contributors are their own.
xMatters delivers integration-driven collaboration that relays data between systems, while engaging the
right people to proactively resolve issues. Read the Monitoring in a Connected Enterprise
whitepaper and learn about 3 tools for resolving incidents quickly.
OptaPlanner, an open-source constraint resolver, inally supports multithreaded incremental solving. The
speedup is spectacular. Even with just a few CPU cores, it triples the score calculation speed — see the results
below. To activate it, a single extra line in the con iguration suf ices.
The original feature request stems from 2007. Throughout the years, step by step, we diligently prepared the
internal architecture for it, so now, after 10 years, we fully support it from 7.9.0.Final onwards.
Requirements
Let’s take a look at the requirements for multithreaded incremental solving:
h l l l h d h
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement d l l h d d l 15/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
There are several ways to use multiple threads without doing real multithreaded solving:
Partitioned search: Split up one dataset and solve each one separately.
Fully supported since OptaPlanner 7.0.
Scales horizontally at an expensive trade-off of solution quality, because partitioning excludes
optimal solutions.
But none of these are real parallel heuristics, as shown in the bottom right corner below:
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 16/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
Multithreaded solving is just a matter of distributing the move evaluations of a step across multiple threads.
That’s straightforward. There are even a few users that did this (most notably a space agency supplier), by
hacking our code. But they didn’t see a performance gain. Quite the opposite actually (except with an easy
score calculator). Those changes broke incremental score calculation. Multithreaded solving is easy. But
multithreaded incremental solving is hard.
Ah, this brings us to incremental score calculation. The key to performance. It is the rocket science at the
heart of OptaPlanner that brings massive scalability. And — for the few that have seen them — the cause of
the notorious score corruption exception.
What is incremental score calculation? For each move, we calculate the score of the solution state after
applying that move. With non-incremental score calculation, the entire score is calculated from scratch. But
with incremental score calculation, we only calculate the delta, as shown below. That’s far more ef icient.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 17/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
Of course, now, we can have our cake and eat it, too.
Each incremental score calculator is inherently single-threaded, so each move thread has its own score
calculator and its own solution state. Cloning either is too expensive. To evaluate a move on a move thread,
with incremental score calculation, we must reuse the score calculator of the previous evaluation. This
implies that the working solution must be in the exact same state to begin with. But because the outer step
iterations change the solution state constantly, the move threads must sync up with the main solver thread
after every step.
It’s similar to any real-time multiplayer game (such as StarCraft), in which multiple hosts need to sync up to
show the same game state, but can’t afford to transmit the entire game state for every change.
As soon as one thread goes out of sync, all calculations of that thread are corrupted, and the entire system is
affected. But through a well-designed orchestration of concurrent components (and multi-day test runs), we
prevent race conditions. And it works. Like a charm.
Furthermore, the threads must be able to send moves to each other, even if it’s only to share the winning
move. This, too, posed a challenge. OptaPlanner is an Object Oriented constraint solver, so its decision
variables can be any valid Java type (not just booleans, numbers and loats), such as Employee or Foo . Those
variables can sit in any domain class (called planning entities), such as Shift or Bar . The move instances
reference those class instances. When a solution gets cloned to initiate a move thread, those planning entities,
such as Shift get cloned too. So when a move from thread A gets sent to thread B, OptaPlanner rebases the
move on the solution state of thread B. This replaces the references from the move instance to thread A’s
solution state with the equivalent references of thread B’s solution state. Pretty nifty.
Reproducibility is king. The ability to run the same dataset through OptaPlanner twice and get the exact same
result after the same number of steps (and at every step), is worth its weight in gold. To lose that, would
make debugging, issue tracking and production audits extremely dif icult.
The inherent unpredictable nature of thread execution order on multi-core machines makes reproducibility
an interesting requirement. Combine that with the reliance of many optimization algorithms on a seeded
random number generator (which is not thread-safe), for a real challenge.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 18/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
But we did it. We have 100% reproducibility. This involves several ingenious mechanisms, such as using a
master seeded random to generate a seeded random per thread, generating a predictable number of selected,
Complete Guide to Preventing ATO
buffered moves (because move generation often relies on the random generator too) and reordering
• Measure the Impact of Account Takeover
evaluated moves in their originally selected order when they come back from the move threads.
Download Free PDF
• Learn How to Spot Account Takeover Attempts
• Implement Smart Account Takeover Prevention
The Configuration
Multithreaded incremental solving is easy to activate. Just add a <moveThreadCount> line in your solver con ig:
1 <solver>
2 <moveThreadCount>4</moveThreadCount>
3 ...
4 </solver>
This basically donates 4 extra CPU cores to the solver. Use AUTO to have OptaPlanner deduce it automatically.
Optionally, specify a <threadFactoryClass> for environments that don’t like arbitrary thread creation.
It combines with every other feature, including other multithreading strategies (such as multitenancy,
Partitioned Search, …), if you have enough CPU cores to pull it off.
The Benchmarks
Methodology
Below are the results on different VRP datasets for a First Fit Decreasing (the Construction Heuristic)
followed by Tabu Search (the Local Search). Higher is better.
Complete Guide to Preventing ATO
• Measure the Impact of Account Takeover Download Free PDF
• Learn How to Spot Account Takeover Attempts
• Implement Smart Account Takeover Prevention
The blue bar is the traditional, single-threaded OptaPlanner. It has an average score calculation speed of
26,947 moves per second. That goes up to 45,565 with 2 move threads, to 80,757 with 4 move threads and
to 88,410 with 6 move threads.
So, by donating more CPU cores to OptaPlanner, it uses a fraction of the time to reach the same result.
On other Local Search algorithms, such as Late Acceptance, we see similar results:
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 20/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
We also see a slight reduction of the relative speed gain on the biggest dataset with 2750 VRP locations, but I
suspect this might be because the 4GB max heap memory is too low for it to function at full ef iciency. I’ll
investigate this further.
I also ran benchmarks on the nurse rostering use case, but with a JVM max heap ( -Xmx ) set to 2GB. Here I
tried Tabu Search, Simulated Annealing and Late Acceptance:
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 21/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
In all three cases, we see a welcome speed gain, but Tabu Search (a slow stepping algorithm) has a bigger
relative gain than the others (which are fast stepping algorithms).
In any case, it’s clear that your mileage may vary, depending on the use case and other factors.
Future Improvements
As we increase the number of move threads or decrease the time to evaluate a single move on one thread, we
see a higher congestion on the inter-thread communication queues, leading to a lower relative scalability
gain. There are several ways to deal with that and we’ll be investigating such internal improvements in the
future.
Conclusion
All your CPU are belong to OptaPlanner.
With a single extra con iguration line, OptaPlanner can reach the same high-quality solution in a fraction of
the time. If you have CPU cores to spare, of course.
Discovering, responding to, and resolving incidents is a complex endeavor. Read this narrative to learn
how you can do it quickly and effectively by connecting AppDynamics, Moogsoft and xMatters to create
a monitoring toolchain.
Long Code vs. Short Code: What’s Better for The Power of Generic Algorithms
My Use Case?
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 22/23
9/24/2018 JavaScript: Break Your Code With the Debugger Statement - DZone Performance
Published at DZone with permission of Geoffrey De Smet . See the original article here.
Complete Guide to Preventing ATO
Opinions expressed by DZone contributors are their own.
https://dzone.com/articles/javascript-break-your-code-with-debugger-statement 23/23