You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Profiler.java 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client;
  17. import java.util.ArrayList;
  18. import java.util.Collection;
  19. import java.util.Collections;
  20. import java.util.HashMap;
  21. import java.util.HashSet;
  22. import java.util.LinkedHashMap;
  23. import java.util.LinkedList;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.logging.Logger;
  28. import com.google.gwt.core.client.Duration;
  29. import com.google.gwt.core.client.GWT;
  30. import com.google.gwt.core.client.JavaScriptObject;
  31. import com.google.gwt.core.client.JsArray;
  32. /**
  33. * Lightweight profiling tool that can be used to collect profiling data with
  34. * zero overhead unless enabled. To enable profiling, add
  35. * <code>&lt;set-property name="vaadin.profiler" value="true" /&gt;</code> to
  36. * your .gwt.xml file.
  37. *
  38. * @author Vaadin Ltd
  39. * @since 7.0.0
  40. */
  41. public class Profiler {
  42. private static RelativeTimeSupplier RELATIVE_TIME_SUPPLIER;
  43. private static final String EVT_GROUP = "VaadinProfiler";
  44. private static ProfilerResultConsumer consumer;
  45. /**
  46. * Class to include using deferred binding to enable the profiling.
  47. *
  48. * @author Vaadin Ltd
  49. * @since 7.0.0
  50. */
  51. public static class EnabledProfiler extends Profiler {
  52. @Override
  53. protected boolean isImplEnabled() {
  54. return true;
  55. }
  56. }
  57. /**
  58. * Interface for getting data from the {@link Profiler}.
  59. * <p>
  60. * <b>Warning!</b> This interface is most likely to change in the future
  61. *
  62. * @since 7.1
  63. * @author Vaadin Ltd
  64. */
  65. public interface ProfilerResultConsumer {
  66. public void addProfilerData(Node rootNode, List<Node> totals);
  67. public void addBootstrapData(LinkedHashMap<String, Double> timings);
  68. }
  69. /**
  70. * A hierarchical representation of the time spent running a named block of
  71. * code.
  72. * <p>
  73. * <b>Warning!</b> This class is most likely to change in the future and is
  74. * therefore defined in this class in an internal package instead of
  75. * Profiler where it might seem more logical.
  76. */
  77. public static class Node {
  78. private final String name;
  79. private final LinkedHashMap<String, Node> children = new LinkedHashMap<>();
  80. private double time = 0;
  81. private int count = 0;
  82. private double enterTime = 0;
  83. private double minTime = 1000000000;
  84. private double maxTime = 0;
  85. /**
  86. * Create a new node with the given name.
  87. *
  88. * @param name
  89. */
  90. public Node(String name) {
  91. this.name = name;
  92. }
  93. /**
  94. * Gets the name of the node.
  95. *
  96. * @return the name of the node
  97. */
  98. public String getName() {
  99. return name;
  100. }
  101. /**
  102. * Creates a new child node or retrieves and existing child and updates
  103. * its total time and hit count.
  104. *
  105. * @param name
  106. * the name of the child
  107. * @param timestamp
  108. * the timestamp for when the node is entered
  109. * @return the child node object
  110. */
  111. public Node enterChild(String name, double timestamp) {
  112. Node child = children.get(name);
  113. if (child == null) {
  114. child = new Node(name);
  115. children.put(name, child);
  116. }
  117. child.enterTime = timestamp;
  118. child.count++;
  119. return child;
  120. }
  121. /**
  122. * Gets the total time spent in this node, including time spent in sub
  123. * nodes.
  124. *
  125. * @return the total time spent, in milliseconds
  126. */
  127. public double getTimeSpent() {
  128. return time;
  129. }
  130. /**
  131. * Gets the minimum time spent for one invocation of this node,
  132. * including time spent in sub nodes.
  133. *
  134. * @return the time spent for the fastest invocation, in milliseconds
  135. */
  136. public double getMinTimeSpent() {
  137. return minTime;
  138. }
  139. /**
  140. * Gets the maximum time spent for one invocation of this node,
  141. * including time spent in sub nodes.
  142. *
  143. * @return the time spent for the slowest invocation, in milliseconds
  144. */
  145. public double getMaxTimeSpent() {
  146. return maxTime;
  147. }
  148. /**
  149. * Gets the number of times this node has been entered.
  150. *
  151. * @return the number of times the node has been entered
  152. */
  153. public int getCount() {
  154. return count;
  155. }
  156. /**
  157. * Gets the total time spent in this node, excluding time spent in sub
  158. * nodes.
  159. *
  160. * @return the total time spent, in milliseconds
  161. */
  162. public double getOwnTime() {
  163. double time = getTimeSpent();
  164. for (Node node : children.values()) {
  165. time -= node.getTimeSpent();
  166. }
  167. return time;
  168. }
  169. /**
  170. * Gets the child nodes of this node.
  171. *
  172. * @return a collection of child nodes
  173. */
  174. public Collection<Node> getChildren() {
  175. return Collections.unmodifiableCollection(children.values());
  176. }
  177. @Override
  178. public String toString() {
  179. return getStringRepresentation("");
  180. }
  181. public String getStringRepresentation(String prefix) {
  182. if (getName() == null) {
  183. return "";
  184. }
  185. String msg = prefix + " " + getName() + " in "
  186. + roundToSignificantFigures(getTimeSpent()) + " ms.";
  187. if (getCount() > 1) {
  188. msg += " Invoked " + getCount() + " times ("
  189. + roundToSignificantFigures(getTimeSpent() / getCount())
  190. + " ms per time, min "
  191. + roundToSignificantFigures(getMinTimeSpent())
  192. + " ms, max "
  193. + roundToSignificantFigures(getMaxTimeSpent())
  194. + " ms).";
  195. }
  196. if (!children.isEmpty()) {
  197. double ownTime = getOwnTime();
  198. msg += " " + roundToSignificantFigures(ownTime)
  199. + " ms spent in own code";
  200. if (getCount() > 1) {
  201. msg += " ("
  202. + roundToSignificantFigures(ownTime / getCount())
  203. + " ms per time)";
  204. }
  205. msg += '.';
  206. }
  207. return msg;
  208. }
  209. private static double roundToSignificantFigures(double num) {
  210. // Number of significant digits
  211. int n = 3;
  212. if (num == 0) {
  213. return 0;
  214. }
  215. final double d = Math.ceil(Math.log10(num < 0 ? -num : num));
  216. final int power = n - (int) d;
  217. final double magnitude = Math.pow(10, power);
  218. final long shifted = Math.round(num * magnitude);
  219. return shifted / magnitude;
  220. }
  221. public void sumUpTotals(Map<String, Node> totals) {
  222. String name = getName();
  223. if (name != null) {
  224. Node totalNode = totals.get(name);
  225. if (totalNode == null) {
  226. totalNode = new Node(name);
  227. totals.put(name, totalNode);
  228. }
  229. totalNode.time += getOwnTime();
  230. totalNode.count += getCount();
  231. totalNode.minTime = roundToSignificantFigures(
  232. Math.min(totalNode.minTime, getMinTimeSpent()));
  233. totalNode.maxTime = roundToSignificantFigures(
  234. Math.max(totalNode.maxTime, getMaxTimeSpent()));
  235. }
  236. for (Node node : children.values()) {
  237. node.sumUpTotals(totals);
  238. }
  239. }
  240. /**
  241. * @param timestamp
  242. */
  243. public void leave(double timestamp) {
  244. double elapsed = (timestamp - enterTime);
  245. time += elapsed;
  246. enterTime = 0;
  247. if (elapsed < minTime) {
  248. minTime = elapsed;
  249. }
  250. if (elapsed > maxTime) {
  251. maxTime = elapsed;
  252. }
  253. }
  254. }
  255. private static final class GwtStatsEvent extends JavaScriptObject {
  256. protected GwtStatsEvent() {
  257. // JSO constructor
  258. }
  259. private native String getEvtGroup()
  260. /*-{
  261. return this.evtGroup;
  262. }-*/;
  263. private native double getMillis()
  264. /*-{
  265. return this.millis;
  266. }-*/;
  267. private native String getSubSystem()
  268. /*-{
  269. return this.subSystem;
  270. }-*/;
  271. private native String getType()
  272. /*-{
  273. return this.type;
  274. }-*/;
  275. private native String getModuleName()
  276. /*-{
  277. return this.moduleName;
  278. }-*/;
  279. private native double getRelativeMillis()
  280. /*-{
  281. return this.relativeMillis;
  282. }-*/;
  283. private native boolean isExtendedEvent()
  284. /*-{
  285. return 'relativeMillis' in this;
  286. }-*/;
  287. public final String getEventName() {
  288. String group = getEvtGroup();
  289. if (EVT_GROUP.equals(group)) {
  290. return getSubSystem();
  291. } else {
  292. return group + "." + getSubSystem();
  293. }
  294. }
  295. }
  296. /**
  297. * Checks whether the profiling gathering is enabled.
  298. *
  299. * @return <code>true</code> if the profiling is enabled, else
  300. * <code>false</code>
  301. */
  302. public static boolean isEnabled() {
  303. // This will be fully inlined by the compiler
  304. Profiler create = GWT.create(Profiler.class);
  305. return create.isImplEnabled();
  306. }
  307. /**
  308. * Enters a named block. There should always be a matching invocation of
  309. * {@link #leave(String)} when leaving the block. Calls to this method will
  310. * be removed by the compiler unless profiling is enabled.
  311. *
  312. * @param name
  313. * the name of the entered block
  314. */
  315. public static void enter(String name) {
  316. if (isEnabled()) {
  317. logGwtEvent(name, "begin");
  318. }
  319. }
  320. /**
  321. * Leaves a named block. There should always be a matching invocation of
  322. * {@link #enter(String)} when entering the block. Calls to this method will
  323. * be removed by the compiler unless profiling is enabled.
  324. *
  325. * @param name
  326. * the name of the left block
  327. */
  328. public static void leave(String name) {
  329. if (isEnabled()) {
  330. logGwtEvent(name, "end");
  331. }
  332. }
  333. /**
  334. * Returns time relative to the particular page load time. The value should
  335. * not be used directly but rather difference between two values returned by
  336. * this method should be used to compare measurements.
  337. *
  338. * @since 7.6
  339. */
  340. public static double getRelativeTimeMillis() {
  341. return RELATIVE_TIME_SUPPLIER.getRelativeTime();
  342. }
  343. private static final native void logGwtEvent(String name, String type)
  344. /*-{
  345. $wnd.__gwtStatsEvent({
  346. evtGroup: @com.vaadin.client.Profiler::EVT_GROUP,
  347. moduleName: @com.google.gwt.core.client.GWT::getModuleName()(),
  348. millis: (new Date).getTime(),
  349. sessionId: undefined,
  350. subSystem: name,
  351. type: type,
  352. relativeMillis: @com.vaadin.client.Profiler::getRelativeTimeMillis()()
  353. });
  354. }-*/;
  355. /**
  356. * Resets the collected profiler data. Calls to this method will be removed
  357. * by the compiler unless profiling is enabled.
  358. */
  359. public static void reset() {
  360. if (isEnabled()) {
  361. /*
  362. * Old implementations might call reset for initialization, so
  363. * ensure it is initialized here as well. Initialization has no side
  364. * effects if already done.
  365. */
  366. initialize();
  367. clearEventsList();
  368. }
  369. }
  370. /**
  371. * Initializes the profiler. This should be done before calling any other
  372. * function in this class. Failing to do so might cause undesired behavior.
  373. * This method has no side effects if the initialization has already been
  374. * done.
  375. * <p>
  376. * Please note that this method should be called even if the profiler is not
  377. * enabled because it will then remove a logger function that might have
  378. * been included in the HTML page and that would leak memory unless removed.
  379. * </p>
  380. *
  381. * @since 7.0.2
  382. */
  383. public static void initialize() {
  384. if (hasHighPrecisionTime()) {
  385. RELATIVE_TIME_SUPPLIER = new HighResolutionTimeSupplier();
  386. } else {
  387. RELATIVE_TIME_SUPPLIER = new DefaultRelativeTimeSupplier();
  388. }
  389. if (isEnabled()) {
  390. ensureLogger();
  391. } else {
  392. ensureNoLogger();
  393. }
  394. }
  395. /**
  396. * Outputs the gathered profiling data to the debug console.
  397. */
  398. public static void logTimings() {
  399. if (!isEnabled()) {
  400. getLogger().warning(
  401. "Profiler is not enabled, no data has been collected.");
  402. return;
  403. }
  404. LinkedList<Node> stack = new LinkedList<>();
  405. Node rootNode = new Node(null);
  406. stack.add(rootNode);
  407. JsArray<GwtStatsEvent> gwtStatsEvents = getGwtStatsEvents();
  408. if (gwtStatsEvents.length() == 0) {
  409. getLogger().warning(
  410. "No profiling events recorded, this might happen if another __gwtStatsEvent handler is installed.");
  411. return;
  412. }
  413. Set<Node> extendedTimeNodes = new HashSet<>();
  414. for (int i = 0; i < gwtStatsEvents.length(); i++) {
  415. GwtStatsEvent gwtStatsEvent = gwtStatsEvents.get(i);
  416. if (!EVT_GROUP.equals(gwtStatsEvent.getEvtGroup())) {
  417. // Only log our own events to avoid problems with events which
  418. // are not of type start+end
  419. continue;
  420. }
  421. String eventName = gwtStatsEvent.getEventName();
  422. String type = gwtStatsEvent.getType();
  423. boolean isExtendedEvent = gwtStatsEvent.isExtendedEvent();
  424. boolean isBeginEvent = "begin".equals(type);
  425. Node stackTop = stack.getLast();
  426. boolean inEvent = eventName.equals(stackTop.getName())
  427. && !isBeginEvent;
  428. if (!inEvent && stack.size() >= 2
  429. && eventName.equals(stack.get(stack.size() - 2).getName())
  430. && !isBeginEvent) {
  431. // back out of sub event
  432. if (extendedTimeNodes.contains(stackTop) && isExtendedEvent) {
  433. stackTop.leave(gwtStatsEvent.getRelativeMillis());
  434. } else {
  435. stackTop.leave(gwtStatsEvent.getMillis());
  436. }
  437. stack.removeLast();
  438. stackTop = stack.getLast();
  439. inEvent = true;
  440. }
  441. if (type.equals("end")) {
  442. if (!inEvent) {
  443. getLogger().severe("Got end event for " + eventName
  444. + " but is currently in " + stackTop.getName());
  445. return;
  446. }
  447. Node previousStackTop = stack.removeLast();
  448. if (extendedTimeNodes.contains(previousStackTop)) {
  449. previousStackTop.leave(gwtStatsEvent.getRelativeMillis());
  450. } else {
  451. previousStackTop.leave(gwtStatsEvent.getMillis());
  452. }
  453. } else {
  454. double millis = isExtendedEvent
  455. ? gwtStatsEvent.getRelativeMillis()
  456. : gwtStatsEvent.getMillis();
  457. if (!inEvent) {
  458. stackTop = stackTop.enterChild(eventName, millis);
  459. stack.add(stackTop);
  460. if (isExtendedEvent) {
  461. extendedTimeNodes.add(stackTop);
  462. }
  463. }
  464. if (!isBeginEvent) {
  465. // Create sub event
  466. Node subNode = stackTop.enterChild(eventName + "." + type,
  467. millis);
  468. if (isExtendedEvent) {
  469. extendedTimeNodes.add(subNode);
  470. }
  471. stack.add(subNode);
  472. }
  473. }
  474. }
  475. if (stack.size() != 1) {
  476. getLogger().warning("Not all nodes are left, the last node is "
  477. + stack.getLast().getName());
  478. return;
  479. }
  480. Map<String, Node> totals = new HashMap<>();
  481. rootNode.sumUpTotals(totals);
  482. List<Node> totalList = new ArrayList<>(totals.values());
  483. Collections.sort(totalList,
  484. (o1, o2) -> (int) (o2.getTimeSpent() - o1.getTimeSpent()));
  485. if (getConsumer() != null) {
  486. getConsumer().addProfilerData(stack.getFirst(), totalList);
  487. }
  488. }
  489. /**
  490. * Overridden in {@link EnabledProfiler} to make {@link #isEnabled()} return
  491. * true if GWT.create returns that class.
  492. *
  493. * @return <code>true</code> if the profiling is enabled, else
  494. * <code>false</code>
  495. */
  496. protected boolean isImplEnabled() {
  497. return false;
  498. }
  499. /**
  500. * Outputs the time passed since various events recored in
  501. * performance.timing if supported by the browser.
  502. */
  503. public static void logBootstrapTimings() {
  504. if (isEnabled()) {
  505. double now = Duration.currentTimeMillis();
  506. String[] keys = { "navigationStart", "unloadEventStart",
  507. "unloadEventEnd", "redirectStart", "redirectEnd",
  508. "fetchStart", "domainLookupStart", "domainLookupEnd",
  509. "connectStart", "connectEnd", "requestStart",
  510. "responseStart", "responseEnd", "domLoading",
  511. "domInteractive", "domContentLoadedEventStart",
  512. "domContentLoadedEventEnd", "domComplete", "loadEventStart",
  513. "loadEventEnd" };
  514. LinkedHashMap<String, Double> timings = new LinkedHashMap<>();
  515. for (String key : keys) {
  516. double value = getPerformanceTiming(key);
  517. if (value == 0) {
  518. // Ignore missing value
  519. continue;
  520. }
  521. timings.put(key, Double.valueOf(now - value));
  522. }
  523. if (timings.isEmpty()) {
  524. getLogger().info(
  525. "Bootstrap timings not supported, please ensure your browser supports performance.timing");
  526. return;
  527. }
  528. if (getConsumer() != null) {
  529. getConsumer().addBootstrapData(timings);
  530. }
  531. }
  532. }
  533. private static final native double getPerformanceTiming(String name)
  534. /*-{
  535. if ($wnd.performance && $wnd.performance.timing && $wnd.performance.timing[name]) {
  536. return $wnd.performance.timing[name];
  537. } else {
  538. return 0;
  539. }
  540. }-*/;
  541. private static native JsArray<GwtStatsEvent> getGwtStatsEvents()
  542. /*-{
  543. return $wnd.vaadin.gwtStatsEvents || [];
  544. }-*/;
  545. /**
  546. * Add logger if it's not already there, also initializing the event array
  547. * if needed.
  548. */
  549. private static native void ensureLogger()
  550. /*-{
  551. if (typeof $wnd.__gwtStatsEvent != 'function') {
  552. if (typeof $wnd.vaadin.gwtStatsEvents != 'object') {
  553. $wnd.vaadin.gwtStatsEvents = [];
  554. }
  555. $wnd.__gwtStatsEvent = function(event) {
  556. $wnd.vaadin.gwtStatsEvents.push(event);
  557. return true;
  558. }
  559. }
  560. }-*/;
  561. /**
  562. * Remove logger function and event array if it seems like the function has
  563. * been added by us.
  564. */
  565. private static native void ensureNoLogger()
  566. /*-{
  567. if (typeof $wnd.vaadin.gwtStatsEvents == 'object') {
  568. delete $wnd.vaadin.gwtStatsEvents;
  569. if (typeof $wnd.__gwtStatsEvent == 'function') {
  570. $wnd.__gwtStatsEvent = function() { return true; };
  571. }
  572. }
  573. }-*/;
  574. private static native JsArray<GwtStatsEvent> clearEventsList()
  575. /*-{
  576. $wnd.vaadin.gwtStatsEvents = [];
  577. }-*/;
  578. /**
  579. * Sets the profiler result consumer that is used to output the profiler
  580. * data to the user.
  581. * <p>
  582. * <b>Warning!</b> This is internal API and should not be used by
  583. * applications or add-ons.
  584. *
  585. * @since 7.1.4
  586. * @param profilerResultConsumer
  587. * the consumer that gets profiler data
  588. */
  589. public static void setProfilerResultConsumer(
  590. ProfilerResultConsumer profilerResultConsumer) {
  591. if (consumer != null) {
  592. throw new IllegalStateException(
  593. "The consumer has already been set");
  594. }
  595. consumer = profilerResultConsumer;
  596. }
  597. private static ProfilerResultConsumer getConsumer() {
  598. return consumer;
  599. }
  600. private static Logger getLogger() {
  601. return Logger.getLogger(Profiler.class.getName());
  602. }
  603. private static native boolean hasHighPrecisionTime()
  604. /*-{
  605. return $wnd.performance && (typeof $wnd.performance.now == 'function');
  606. }-*/;
  607. private interface RelativeTimeSupplier {
  608. double getRelativeTime();
  609. }
  610. private static class DefaultRelativeTimeSupplier
  611. implements RelativeTimeSupplier {
  612. @Override
  613. public native double getRelativeTime()
  614. /*-{
  615. return (new Date).getTime();
  616. }-*/;
  617. }
  618. private static class HighResolutionTimeSupplier
  619. implements RelativeTimeSupplier {
  620. @Override
  621. public native double getRelativeTime()
  622. /*-{
  623. return $wnd.performance.now();
  624. }-*/;
  625. }
  626. }