mirror of
https://github.com/servo/servo.git
synced 2025-08-07 06:25:32 +01:00
Add getBoundingClientRect, and make it and getClientRects synchronously query layout. Associate flows with DOM nodes to allow this querying to occur. Alleviate the problem of Element objects not having access to the original AbstractNode by adding a transient field to Node that is non-null while a node downcast is taking place.
This commit is contained in:
parent
71df18a839
commit
5bade7b0fb
12 changed files with 219 additions and 59 deletions
|
@ -228,7 +228,8 @@ pub impl Content {
|
|||
debug!("js_scripts: %?", js_scripts);
|
||||
|
||||
let window = Window(self.control_chan.clone(),
|
||||
self.event_chan.clone());
|
||||
self.event_chan.clone(),
|
||||
ptr::to_mut_unsafe_ptr(&mut *self)); //FIXME store this safely
|
||||
let document = Document(root, Some(window));
|
||||
|
||||
do root.with_mut_node |node| {
|
||||
|
@ -344,7 +345,7 @@ pub impl Content {
|
|||
}
|
||||
|
||||
fn query_layout(&mut self, query: layout_task::LayoutQuery) -> layout_task::LayoutQueryResponse {
|
||||
self.relayout(self.document.get(), &(copy self.doc_url).get());
|
||||
//self.relayout(self.document.get(), &(copy self.doc_url).get());
|
||||
self.join_layout();
|
||||
|
||||
let (response_port, response_chan) = comm::stream();
|
||||
|
|
|
@ -86,6 +86,11 @@ pub fn init(compartment: @mut Compartment) {
|
|||
nargs: 0,
|
||||
flags: 0,
|
||||
selfHostedName: null()},
|
||||
JSFunctionSpec {name: compartment.add_name(~"getBoundingClientRect"),
|
||||
call: JSNativeWrapper {op: getBoundingClientRect, info: null()},
|
||||
nargs: 0,
|
||||
flags: 0,
|
||||
selfHostedName: null()},
|
||||
JSFunctionSpec {name: compartment.add_name(~"setAttribute"),
|
||||
call: JSNativeWrapper {op: setAttribute, info: null()},
|
||||
nargs: 0,
|
||||
|
@ -137,7 +142,27 @@ extern fn getClientRects(cx: *JSContext, _argc: c_uint, vp: *JSVal) -> JSBool {
|
|||
JS_SET_RVAL(cx, vp, JSVAL_NULL);
|
||||
} else {
|
||||
let cache = node.get_wrappercache();
|
||||
let rval = rval.get() as @mut CacheableWrapper;
|
||||
let rval = rval.get() as @mut CacheableWrapper;
|
||||
assert!(WrapNewBindingObject(cx, cache.get_wrapper(),
|
||||
rval,
|
||||
cast::transmute(vp)));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
extern fn getBoundingClientRect(cx: *JSContext, _argc: c_uint, vp: *JSVal) -> JSBool {
|
||||
unsafe {
|
||||
let obj = JS_THIS_OBJECT(cx, vp);
|
||||
let mut node = unwrap(obj);
|
||||
let rval = do node.with_imm_element |elem| {
|
||||
elem.getBoundingClientRect()
|
||||
};
|
||||
if rval.is_none() {
|
||||
JS_SET_RVAL(cx, vp, JSVAL_NULL);
|
||||
} else {
|
||||
let cache = node.get_wrappercache();
|
||||
let rval = rval.get() as @mut CacheableWrapper;
|
||||
assert!(WrapNewBindingObject(cx, cache.get_wrapper(),
|
||||
rval,
|
||||
cast::transmute(vp)));
|
||||
|
@ -192,7 +217,12 @@ extern fn HTMLImageElement_getWidth(cx: *JSContext, _argc: c_uint, vp: *mut JSVa
|
|||
ElementNodeTypeId(HTMLImageElementTypeId) => {
|
||||
let content = task_from_context(cx);
|
||||
match (*content).query_layout(layout_task::ContentBox(node)) {
|
||||
Ok(rect) => rect.width,
|
||||
Ok(rect) => {
|
||||
match rect {
|
||||
layout_task::ContentRect(rect) => rect.size.width.to_px(),
|
||||
_ => fail!(~"unexpected layout reply")
|
||||
}
|
||||
}
|
||||
Err(()) => 0
|
||||
}
|
||||
// TODO: if nothing is being rendered(?), return zero dimensions
|
||||
|
|
|
@ -3,14 +3,14 @@ use dom::bindings::utils::WrapperCache;
|
|||
|
||||
pub struct ClientRectList {
|
||||
wrapper: WrapperCache,
|
||||
rects: ~[(f32, f32, f32, f32)]
|
||||
rects: ~[@mut ClientRect]
|
||||
}
|
||||
|
||||
pub impl ClientRectList {
|
||||
fn new() -> @mut ClientRectList {
|
||||
fn new(rects: ~[@mut ClientRect]) -> @mut ClientRectList {
|
||||
let list = @mut ClientRectList {
|
||||
wrapper: WrapperCache::new(),
|
||||
rects: ~[(5.6, 80.2, 3.7, 4.8), (800.1, 8001.1, -50.000001, -45.01)]
|
||||
rects: rects
|
||||
};
|
||||
list.init_wrapper();
|
||||
list
|
||||
|
@ -22,8 +22,7 @@ pub impl ClientRectList {
|
|||
|
||||
fn Item(&self, index: u32) -> Option<@mut ClientRect> {
|
||||
if index < self.rects.len() as u32 {
|
||||
let (top, bottom, left, right) = self.rects[index];
|
||||
Some(ClientRect::new(top, bottom, left, right))
|
||||
Some(self.rects[index])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -33,4 +32,4 @@ pub impl ClientRectList {
|
|||
*found = index < self.rects.len() as u32;
|
||||
self.Item(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,12 @@
|
|||
//
|
||||
|
||||
use dom::node::{ElementNodeTypeId, Node};
|
||||
use dom::clientrect::ClientRect;
|
||||
use dom::clientrectlist::ClientRectList;
|
||||
use dom::bindings::utils::DOMString;
|
||||
|
||||
use layout::layout_task;
|
||||
|
||||
use core::str::eq_slice;
|
||||
use core::cell::Cell;
|
||||
use std::net::url::Url;
|
||||
|
@ -157,7 +160,80 @@ pub impl<'self> Element {
|
|||
}
|
||||
|
||||
fn getClientRects(&self) -> Option<@mut ClientRectList> {
|
||||
Some(ClientRectList::new())
|
||||
let rects = match self.parent.owner_doc {
|
||||
Some(doc) => {
|
||||
match doc.window {
|
||||
Some(win) => {
|
||||
let node = self.parent.abstract.get();
|
||||
assert!(node.is_element());
|
||||
let content = unsafe { &mut *win.content_task };
|
||||
match content.query_layout(layout_task::ContentBoxes(node)) {
|
||||
Ok(rects) => match rects {
|
||||
layout_task::ContentRects(rects) =>
|
||||
do rects.map |r| {
|
||||
ClientRect::new(
|
||||
r.origin.y.to_f32(),
|
||||
(r.origin.y + r.size.height).to_f32(),
|
||||
r.origin.x.to_f32(),
|
||||
(r.origin.x + r.size.width).to_f32())
|
||||
},
|
||||
_ => fail!(~"unexpected layout reply")
|
||||
},
|
||||
Err(()) => {
|
||||
debug!("layout query error");
|
||||
~[]
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("no window");
|
||||
~[]
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("no document");
|
||||
~[]
|
||||
}
|
||||
};
|
||||
Some(ClientRectList::new(rects))
|
||||
}
|
||||
|
||||
fn getBoundingClientRect(&self) -> Option<@mut ClientRect> {
|
||||
match self.parent.owner_doc {
|
||||
Some(doc) => {
|
||||
match doc.window {
|
||||
Some(win) => {
|
||||
let node = self.parent.abstract.get();
|
||||
assert!(node.is_element());
|
||||
let content = unsafe { &mut *win.content_task };
|
||||
match content.query_layout(layout_task::ContentBox(node)) {
|
||||
Ok(rect) => match rect {
|
||||
layout_task::ContentRect(rect) =>
|
||||
Some(ClientRect::new(
|
||||
rect.origin.y.to_f32(),
|
||||
(rect.origin.y + rect.size.height).to_f32(),
|
||||
rect.origin.x.to_f32(),
|
||||
(rect.origin.x + rect.size.width).to_f32())),
|
||||
_ => fail!(~"unexpected layout result")
|
||||
},
|
||||
Err(()) => {
|
||||
debug!("error querying layout");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("no window");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("no document");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,8 @@ pub struct Node {
|
|||
wrapper: WrapperCache,
|
||||
type_id: NodeTypeId,
|
||||
|
||||
abstract: Option<AbstractNode>,
|
||||
|
||||
parent_node: Option<AbstractNode>,
|
||||
first_child: Option<AbstractNode>,
|
||||
last_child: Option<AbstractNode>,
|
||||
|
@ -238,15 +240,27 @@ pub impl AbstractNode {
|
|||
|
||||
fn transmute<T, R>(self, f: &fn(&T) -> R) -> R {
|
||||
unsafe {
|
||||
let node_box: *mut bindings::utils::rust_box<Node> = transmute(self.obj);
|
||||
let node = &mut (*node_box).payload;
|
||||
let old = node.abstract;
|
||||
node.abstract = Some(self);
|
||||
let box: *bindings::utils::rust_box<T> = transmute(self.obj);
|
||||
f(&(*box).payload)
|
||||
let rv = f(&(*box).payload);
|
||||
node.abstract = old;
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
fn transmute_mut<T, R>(self, f: &fn(&mut T) -> R) -> R {
|
||||
unsafe {
|
||||
let node_box: *mut bindings::utils::rust_box<Node> = transmute(self.obj);
|
||||
let node = &mut (*node_box).payload;
|
||||
let old = node.abstract;
|
||||
node.abstract = Some(self);
|
||||
let box: *bindings::utils::rust_box<T> = transmute(self.obj);
|
||||
f(cast::transmute(&(*box).payload))
|
||||
let rv = f(cast::transmute(&(*box).payload));
|
||||
node.abstract = old;
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -381,6 +395,8 @@ impl Node {
|
|||
wrapper: WrapperCache::new(),
|
||||
type_id: type_id,
|
||||
|
||||
abstract: None,
|
||||
|
||||
parent_node: None,
|
||||
first_child: None,
|
||||
last_child: None,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use content::content_task::{ControlMsg, Timer, ExitMsg, global_content};
|
||||
use content::content_task::{ControlMsg, Timer, ExitMsg, global_content, Content};
|
||||
use dom::bindings::utils::WrapperCache;
|
||||
use dom::bindings::window;
|
||||
use dom::event::Event;
|
||||
|
@ -19,9 +19,12 @@ pub enum TimerControlMsg {
|
|||
TimerMessage_TriggerExit //XXXjdm this is just a quick hack to talk to the content task
|
||||
}
|
||||
|
||||
//FIXME If we're going to store the content task, find a way to do so safely. Currently it's
|
||||
// only used for querying layout from arbitrary content.
|
||||
pub struct Window {
|
||||
timer_chan: Chan<TimerControlMsg>,
|
||||
dom_event_chan: SharedChan<Event>,
|
||||
content_task: *mut Content,
|
||||
wrapper: WrapperCache
|
||||
}
|
||||
|
||||
|
@ -81,7 +84,8 @@ pub impl Window {
|
|||
}
|
||||
|
||||
pub fn Window(content_chan: comm::SharedChan<ControlMsg>,
|
||||
dom_event_chan: comm::SharedChan<Event>) -> @mut Window {
|
||||
dom_event_chan: comm::SharedChan<Event>,
|
||||
content_task: *mut Content) -> @mut Window {
|
||||
|
||||
let win = @mut Window {
|
||||
wrapper: WrapperCache::new(),
|
||||
|
@ -96,7 +100,8 @@ pub fn Window(content_chan: comm::SharedChan<ControlMsg>,
|
|||
TimerMessage_TriggerExit => content_chan.send(ExitMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
content_task: content_task
|
||||
};
|
||||
let compartment = global_content().compartment.get();
|
||||
window::create(compartment, win);
|
||||
|
|
|
@ -230,16 +230,17 @@ impl BuilderContext {
|
|||
|
||||
priv fn create_child_flow_of_type(&self,
|
||||
flow_type: FlowContextType,
|
||||
builder: &mut LayoutTreeBuilder) -> BuilderContext {
|
||||
let new_flow = builder.make_flow(flow_type);
|
||||
builder: &mut LayoutTreeBuilder,
|
||||
node: AbstractNode) -> BuilderContext {
|
||||
let new_flow = builder.make_flow(flow_type, node);
|
||||
self.attach_child_flow(new_flow);
|
||||
|
||||
BuilderContext::new(@mut BoxGenerator::new(new_flow))
|
||||
}
|
||||
|
||||
priv fn make_inline_collector(&mut self, builder: &mut LayoutTreeBuilder) -> BuilderContext {
|
||||
priv fn make_inline_collector(&mut self, builder: &mut LayoutTreeBuilder, node: AbstractNode) -> BuilderContext {
|
||||
debug!("BuilderContext: making new inline collector flow");
|
||||
let new_flow = builder.make_flow(Flow_Inline);
|
||||
let new_flow = builder.make_flow(Flow_Inline, node);
|
||||
let new_generator = @mut BoxGenerator::new(new_flow);
|
||||
|
||||
self.inline_collector = Some(new_generator);
|
||||
|
@ -248,10 +249,10 @@ impl BuilderContext {
|
|||
BuilderContext::new(new_generator)
|
||||
}
|
||||
|
||||
priv fn get_inline_collector(&mut self, builder: &mut LayoutTreeBuilder) -> BuilderContext {
|
||||
priv fn get_inline_collector(&mut self, builder: &mut LayoutTreeBuilder, node: AbstractNode) -> BuilderContext {
|
||||
match copy self.inline_collector {
|
||||
Some(collector) => BuilderContext::new(collector),
|
||||
None => self.make_inline_collector(builder)
|
||||
None => self.make_inline_collector(builder, node)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,18 +279,18 @@ impl BuilderContext {
|
|||
// If this is the root node, then use the root flow's
|
||||
// context. Otherwise, make a child block context.
|
||||
match node.parent_node() {
|
||||
Some(_) => { self.create_child_flow_of_type(Flow_Block, builder) }
|
||||
Some(_) => { self.create_child_flow_of_type(Flow_Block, builder, node) }
|
||||
None => { self.clone() },
|
||||
}
|
||||
},
|
||||
(CSSDisplayBlock, @BlockFlow(*)) => {
|
||||
self.clear_inline_collector();
|
||||
self.create_child_flow_of_type(Flow_Block, builder)
|
||||
self.create_child_flow_of_type(Flow_Block, builder, node)
|
||||
},
|
||||
(CSSDisplayInline, @InlineFlow(*)) => self.clone(),
|
||||
(CSSDisplayInlineBlock, @InlineFlow(*)) => self.clone(),
|
||||
(CSSDisplayInline, @BlockFlow(*)) => self.get_inline_collector(builder),
|
||||
(CSSDisplayInlineBlock, @BlockFlow(*)) => self.get_inline_collector(builder),
|
||||
(CSSDisplayInline, @BlockFlow(*)) => self.get_inline_collector(builder, node),
|
||||
(CSSDisplayInlineBlock, @BlockFlow(*)) => self.get_inline_collector(builder, node),
|
||||
_ => self.clone()
|
||||
};
|
||||
|
||||
|
@ -332,10 +333,9 @@ pub impl LayoutTreeBuilder {
|
|||
// nodes and FlowContexts should not change during layout.
|
||||
let flow = &mut this_ctx.default_collector.flow;
|
||||
for tree::each_child(&FlowTree, flow) |child_flow: &@mut FlowContext| {
|
||||
for (copy child_flow.d().node).each |node| {
|
||||
assert!(node.has_layout_data());
|
||||
node.layout_data().flow = Some(*child_flow);
|
||||
}
|
||||
let node = child_flow.d().node;
|
||||
assert!(node.has_layout_data());
|
||||
node.layout_data().flow = Some(*child_flow);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -406,7 +406,7 @@ pub impl LayoutTreeBuilder {
|
|||
called on root DOM element. */
|
||||
fn construct_trees(&mut self, layout_ctx: &LayoutContext, root: AbstractNode)
|
||||
-> Result<@mut FlowContext, ()> {
|
||||
let new_flow = self.make_flow(Flow_Root);
|
||||
let new_flow = self.make_flow(Flow_Root, root);
|
||||
let new_generator = @mut BoxGenerator::new(new_flow);
|
||||
let mut root_ctx = BuilderContext::new(new_generator);
|
||||
|
||||
|
@ -415,8 +415,8 @@ pub impl LayoutTreeBuilder {
|
|||
return Ok(new_flow)
|
||||
}
|
||||
|
||||
fn make_flow(&mut self, ty: FlowContextType) -> @mut FlowContext {
|
||||
let data = FlowData(self.next_flow_id());
|
||||
fn make_flow(&mut self, ty: FlowContextType, node: AbstractNode) -> @mut FlowContext {
|
||||
let data = FlowData(self.next_flow_id(), node);
|
||||
let ret = match ty {
|
||||
Flow_Absolute => @mut AbsoluteFlow(data),
|
||||
Flow_Block => @mut BlockFlow(data, BlockFlowData()),
|
||||
|
|
|
@ -70,7 +70,7 @@ pub enum FlowContextType {
|
|||
/* A particular kind of layout context. It manages the positioning of
|
||||
render boxes within the context. */
|
||||
pub struct FlowData {
|
||||
node: Option<AbstractNode>,
|
||||
node: AbstractNode,
|
||||
/* reference to parent, children flow contexts */
|
||||
tree: tree::Tree<@mut FlowContext>,
|
||||
/* TODO (Issue #87): debug only */
|
||||
|
@ -84,9 +84,9 @@ pub struct FlowData {
|
|||
position: Rect<Au>,
|
||||
}
|
||||
|
||||
pub fn FlowData(id: int) -> FlowData {
|
||||
pub fn FlowData(id: int, node: AbstractNode) -> FlowData {
|
||||
FlowData {
|
||||
node: None,
|
||||
node: node,
|
||||
tree: tree::empty(),
|
||||
id: id,
|
||||
|
||||
|
|
|
@ -40,13 +40,15 @@ use std::net::url::Url;
|
|||
pub type LayoutTask = SharedChan<Msg>;
|
||||
|
||||
pub enum LayoutQuery {
|
||||
ContentBox(AbstractNode)
|
||||
ContentBox(AbstractNode),
|
||||
ContentBoxes(AbstractNode)
|
||||
}
|
||||
|
||||
pub type LayoutQueryResponse = Result<LayoutQueryResponse_, ()>;
|
||||
|
||||
enum LayoutQueryResponse_ {
|
||||
ContentSize(Size2D<int>)
|
||||
pub enum LayoutQueryResponse_ {
|
||||
ContentRect(Rect<Au>),
|
||||
ContentRects(~[Rect<Au>])
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
|
@ -256,7 +258,10 @@ impl Layout {
|
|||
match query {
|
||||
ContentBox(node) => {
|
||||
let response = match node.layout_data().flow {
|
||||
None => Err(()),
|
||||
None => {
|
||||
error!("no flow present");
|
||||
Err(())
|
||||
}
|
||||
Some(flow) => {
|
||||
let start_val: Option<Rect<Au>> = None;
|
||||
let rect = do flow.foldl_boxes_for_node(node, start_val) |acc, box| {
|
||||
|
@ -267,16 +272,30 @@ impl Layout {
|
|||
};
|
||||
|
||||
match rect {
|
||||
None => Err(()),
|
||||
Some(rect) => {
|
||||
let size = Size2D(rect.size.width.to_px(),
|
||||
rect.size.height.to_px());
|
||||
Ok(ContentSize(size))
|
||||
None => {
|
||||
error!("no boxes for node");
|
||||
Err(())
|
||||
}
|
||||
Some(rect) => Ok(ContentRect(rect))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reply_chan.send(response)
|
||||
}
|
||||
ContentBoxes(node) => {
|
||||
let response = match node.layout_data().flow {
|
||||
None => Err(()),
|
||||
Some(flow) => {
|
||||
let mut boxes = ~[];
|
||||
for flow.iter_boxes_for_node(node) |box| {
|
||||
boxes.push(box.content_box());
|
||||
}
|
||||
|
||||
Ok(ContentRects(boxes))
|
||||
}
|
||||
};
|
||||
|
||||
reply_chan.send(response)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<script src="test_bindings.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<span id="third"></div>
|
||||
<div id="fourth"></div>
|
||||
<div id="first">fffff<br><br><br><br>fffffffffffffffff</div>
|
||||
<div id="second">ggg</div>
|
||||
<span id="third">hhhhhhhh</span>
|
||||
<div id="fourth">iiiiiiiiiiiiiiiiiii</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
//window.alert(ClientRect);
|
||||
//window.alert(ClientRectList);
|
||||
|
||||
window.alert("1");
|
||||
let elem = document.documentElement;
|
||||
window.alert("==1==");
|
||||
let elem = document.getElementsByTagName('div')[0];
|
||||
window.alert(elem.nodeType);
|
||||
window.alert(elem);
|
||||
window.alert("2");
|
||||
window.alert("==1.5==");
|
||||
var rect = elem.getBoundingClientRect();
|
||||
window.alert(rect);
|
||||
window.alert(rect.top);
|
||||
window.alert(rect.bottom);
|
||||
window.alert(rect.left);
|
||||
window.alert(rect.right);
|
||||
window.alert(rect.width);
|
||||
window.alert(rect.height);
|
||||
window.alert("==2==");
|
||||
var rects = elem.getClientRects();
|
||||
window.alert("3");
|
||||
window.alert("==3==");
|
||||
window.alert(rects);
|
||||
window.alert(rects.length);
|
||||
window.alert("4");
|
||||
window.alert("==4==");
|
||||
let rect = rects[0];
|
||||
window.alert(rect);
|
||||
/*window.alert(Object.prototype.toString.call(rect.__proto__));
|
||||
|
@ -40,4 +48,4 @@ window.alert("DOMParser:");
|
|||
window.alert(DOMParser);
|
||||
let parser = new DOMParser();
|
||||
window.alert(parser);
|
||||
window.alert(parser.parseFromString("<html></html>", "text/html"));
|
||||
//window.alert(parser.parseFromString("<html></html>", "text/html"));
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
var divs = document.getElementsByTagName("div");
|
||||
var div = divs[0];
|
||||
for (var i = 0; i < 1000000; i++) {
|
||||
div.setAttribute('id', 'styled');
|
||||
}
|
||||
|
||||
var count = 1000000;
|
||||
var start = new Date();
|
||||
for (var i = 0; i < count; i++) {
|
||||
div.setAttribute('id', 'styled');
|
||||
div.getBoundingClientRect();
|
||||
window.alert(i);
|
||||
}
|
||||
var stop = new Date();
|
||||
window.alert((stop - start) / count * 1e6);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue