1 | /* |
2 | * Copyright (C) 2011 Apple Inc. All rights reserved. |
3 | * Copyright (C) 2013 Nokia Corporation and/or its subsidiary(-ies). |
4 | * Copyright (C) 2016-2019 Igalia S.L. |
5 | * |
6 | * Redistribution and use in source and binary forms, with or without |
7 | * modification, are permitted provided that the following conditions |
8 | * are met: |
9 | * 1. Redistributions of source code must retain the above copyright |
10 | * notice, this list of conditions and the following disclaimer. |
11 | * 2. Redistributions in binary form must reproduce the above copyright |
12 | * notice, this list of conditions and the following disclaimer in the |
13 | * documentation and/or other materials provided with the distribution. |
14 | * |
15 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
17 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
18 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
19 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
20 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
21 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
22 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
23 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
24 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
25 | * THE POSSIBILITY OF SUCH DAMAGE. |
26 | */ |
27 | |
28 | #include "config.h" |
29 | #include "DrawingAreaProxyCoordinatedGraphics.h" |
30 | |
31 | #include "DrawingAreaMessages.h" |
32 | #include "DrawingAreaProxyMessages.h" |
33 | #include "LayerTreeContext.h" |
34 | #include "UpdateInfo.h" |
35 | #include "WebPageProxy.h" |
36 | #include "WebPreferences.h" |
37 | #include "WebProcessProxy.h" |
38 | #include <WebCore/Region.h> |
39 | |
40 | #if PLATFORM(GTK) |
41 | #include <gtk/gtk.h> |
42 | #endif |
43 | |
44 | #if PLATFORM(WAYLAND) |
45 | #include "WaylandCompositor.h" |
46 | #include <WebCore/PlatformDisplay.h> |
47 | #endif |
48 | |
49 | #if USE(GLIB_EVENT_LOOP) |
50 | #include <wtf/glib/RunLoopSourcePriority.h> |
51 | #endif |
52 | |
53 | namespace WebKit { |
54 | using namespace WebCore; |
55 | |
56 | DrawingAreaProxyCoordinatedGraphics::DrawingAreaProxyCoordinatedGraphics(WebPageProxy& webPageProxy, WebProcessProxy& process) |
57 | : DrawingAreaProxy(DrawingAreaTypeCoordinatedGraphics, webPageProxy, process) |
58 | #if !PLATFORM(WPE) |
59 | , m_discardBackingStoreTimer(RunLoop::current(), this, &DrawingAreaProxyCoordinatedGraphics::discardBackingStore) |
60 | #endif |
61 | { |
62 | #if USE(GLIB_EVENT_LOOP) && !PLATFORM(WPE) |
63 | m_discardBackingStoreTimer.setPriority(RunLoopSourcePriority::ReleaseUnusedResourcesTimer); |
64 | #endif |
65 | } |
66 | |
67 | DrawingAreaProxyCoordinatedGraphics::~DrawingAreaProxyCoordinatedGraphics() |
68 | { |
69 | // Make sure to exit accelerated compositing mode. |
70 | if (isInAcceleratedCompositingMode()) |
71 | exitAcceleratedCompositingMode(); |
72 | } |
73 | |
74 | #if !PLATFORM(WPE) |
75 | void DrawingAreaProxyCoordinatedGraphics::paint(BackingStore::PlatformGraphicsContext context, const IntRect& rect, Region& unpaintedRegion) |
76 | { |
77 | unpaintedRegion = rect; |
78 | |
79 | if (isInAcceleratedCompositingMode()) |
80 | return; |
81 | |
82 | ASSERT(m_currentBackingStoreStateID <= m_nextBackingStoreStateID); |
83 | if (m_currentBackingStoreStateID < m_nextBackingStoreStateID) { |
84 | // Tell the web process to do a full backing store update now, in case we previously told |
85 | // it about our next state but didn't request an immediate update. |
86 | sendUpdateBackingStoreState(RespondImmediately); |
87 | |
88 | // If we haven't yet received our first bits from the WebProcess then don't paint anything. |
89 | if (!m_hasReceivedFirstUpdate) |
90 | return; |
91 | |
92 | if (m_isWaitingForDidUpdateBackingStoreState) { |
93 | // Wait for a DidUpdateBackingStoreState message that contains the new bits before we paint |
94 | // what's currently in the backing store. |
95 | waitForAndDispatchDidUpdateBackingStoreState(); |
96 | } |
97 | |
98 | // Dispatching DidUpdateBackingStoreState (either beneath sendUpdateBackingStoreState or |
99 | // beneath waitForAndDispatchDidUpdateBackingStoreState) could destroy our backing store or |
100 | // change the compositing mode. |
101 | if (!m_backingStore || isInAcceleratedCompositingMode()) |
102 | return; |
103 | } else { |
104 | ASSERT(!m_isWaitingForDidUpdateBackingStoreState); |
105 | if (!m_backingStore) { |
106 | // The view has asked us to paint before the web process has painted anything. There's |
107 | // nothing we can do. |
108 | return; |
109 | } |
110 | } |
111 | |
112 | m_backingStore->paint(context, rect); |
113 | unpaintedRegion.subtract(IntRect(IntPoint(), m_backingStore->size())); |
114 | |
115 | discardBackingStoreSoon(); |
116 | } |
117 | #endif |
118 | |
119 | void DrawingAreaProxyCoordinatedGraphics::sizeDidChange() |
120 | { |
121 | backingStoreStateDidChange(RespondImmediately); |
122 | } |
123 | |
124 | void DrawingAreaProxyCoordinatedGraphics::deviceScaleFactorDidChange() |
125 | { |
126 | backingStoreStateDidChange(RespondImmediately); |
127 | } |
128 | |
129 | void DrawingAreaProxyCoordinatedGraphics::waitForBackingStoreUpdateOnNextPaint() |
130 | { |
131 | m_hasReceivedFirstUpdate = true; |
132 | } |
133 | |
134 | void DrawingAreaProxyCoordinatedGraphics::setBackingStoreIsDiscardable(bool isBackingStoreDiscardable) |
135 | { |
136 | #if !PLATFORM(WPE) |
137 | if (m_isBackingStoreDiscardable == isBackingStoreDiscardable) |
138 | return; |
139 | |
140 | m_isBackingStoreDiscardable = isBackingStoreDiscardable; |
141 | if (m_isBackingStoreDiscardable) |
142 | discardBackingStoreSoon(); |
143 | else |
144 | m_discardBackingStoreTimer.stop(); |
145 | #endif |
146 | } |
147 | |
148 | void DrawingAreaProxyCoordinatedGraphics::update(uint64_t backingStoreStateID, const UpdateInfo& updateInfo) |
149 | { |
150 | ASSERT_ARG(backingStoreStateID, backingStoreStateID <= m_currentBackingStoreStateID); |
151 | if (backingStoreStateID < m_currentBackingStoreStateID) |
152 | return; |
153 | |
154 | // FIXME: Handle the case where the view is hidden. |
155 | |
156 | #if !PLATFORM(WPE) |
157 | incorporateUpdate(updateInfo); |
158 | #endif |
159 | send(Messages::DrawingArea::DidUpdate()); |
160 | } |
161 | |
162 | void DrawingAreaProxyCoordinatedGraphics::didUpdateBackingStoreState(uint64_t backingStoreStateID, const UpdateInfo& updateInfo, const LayerTreeContext& layerTreeContext) |
163 | { |
164 | ASSERT_ARG(backingStoreStateID, backingStoreStateID <= m_nextBackingStoreStateID); |
165 | ASSERT_ARG(backingStoreStateID, backingStoreStateID > m_currentBackingStoreStateID); |
166 | m_currentBackingStoreStateID = backingStoreStateID; |
167 | |
168 | m_isWaitingForDidUpdateBackingStoreState = false; |
169 | |
170 | // Stop the responsiveness timer that was started in sendUpdateBackingStoreState. |
171 | process().responsivenessTimer().stop(); |
172 | |
173 | if (layerTreeContext != m_layerTreeContext) { |
174 | if (layerTreeContext.isEmpty() && !m_layerTreeContext.isEmpty()) { |
175 | exitAcceleratedCompositingMode(); |
176 | ASSERT(m_layerTreeContext.isEmpty()); |
177 | } else if (!layerTreeContext.isEmpty() && m_layerTreeContext.isEmpty()) { |
178 | enterAcceleratedCompositingMode(layerTreeContext); |
179 | ASSERT(layerTreeContext == m_layerTreeContext); |
180 | } else { |
181 | updateAcceleratedCompositingMode(layerTreeContext); |
182 | ASSERT(layerTreeContext == m_layerTreeContext); |
183 | } |
184 | } |
185 | |
186 | if (m_nextBackingStoreStateID != m_currentBackingStoreStateID) |
187 | sendUpdateBackingStoreState(RespondImmediately); |
188 | else { |
189 | m_hasReceivedFirstUpdate = true; |
190 | |
191 | #if USE(TEXTURE_MAPPER_GL) && PLATFORM(GTK) && PLATFORM(X11) && !USE(REDIRECTED_XCOMPOSITE_WINDOW) |
192 | if (m_pendingNativeSurfaceHandleForCompositing) { |
193 | setNativeSurfaceHandleForCompositing(m_pendingNativeSurfaceHandleForCompositing); |
194 | m_pendingNativeSurfaceHandleForCompositing = 0; |
195 | } |
196 | #endif |
197 | } |
198 | |
199 | #if !PLATFORM(WPE) |
200 | if (isInAcceleratedCompositingMode()) { |
201 | ASSERT(!m_backingStore); |
202 | return; |
203 | } |
204 | |
205 | // If we have a backing store the right size, reuse it. |
206 | if (m_backingStore && (m_backingStore->size() != updateInfo.viewSize || m_backingStore->deviceScaleFactor() != updateInfo.deviceScaleFactor)) |
207 | m_backingStore = nullptr; |
208 | incorporateUpdate(updateInfo); |
209 | #endif |
210 | } |
211 | |
212 | void DrawingAreaProxyCoordinatedGraphics::enterAcceleratedCompositingMode(uint64_t backingStoreStateID, const LayerTreeContext& layerTreeContext) |
213 | { |
214 | ASSERT_ARG(backingStoreStateID, backingStoreStateID <= m_currentBackingStoreStateID); |
215 | if (backingStoreStateID < m_currentBackingStoreStateID) |
216 | return; |
217 | |
218 | enterAcceleratedCompositingMode(layerTreeContext); |
219 | } |
220 | |
221 | void DrawingAreaProxyCoordinatedGraphics::exitAcceleratedCompositingMode(uint64_t backingStoreStateID, const UpdateInfo& updateInfo) |
222 | { |
223 | ASSERT_ARG(backingStoreStateID, backingStoreStateID <= m_currentBackingStoreStateID); |
224 | if (backingStoreStateID < m_currentBackingStoreStateID) |
225 | return; |
226 | |
227 | exitAcceleratedCompositingMode(); |
228 | #if !PLATFORM(WPE) |
229 | incorporateUpdate(updateInfo); |
230 | #endif |
231 | } |
232 | |
233 | void DrawingAreaProxyCoordinatedGraphics::updateAcceleratedCompositingMode(uint64_t backingStoreStateID, const LayerTreeContext& layerTreeContext) |
234 | { |
235 | ASSERT_ARG(backingStoreStateID, backingStoreStateID <= m_currentBackingStoreStateID); |
236 | if (backingStoreStateID < m_currentBackingStoreStateID) |
237 | return; |
238 | |
239 | updateAcceleratedCompositingMode(layerTreeContext); |
240 | } |
241 | |
242 | #if !PLATFORM(WPE) |
243 | void DrawingAreaProxyCoordinatedGraphics::incorporateUpdate(const UpdateInfo& updateInfo) |
244 | { |
245 | ASSERT(!isInAcceleratedCompositingMode()); |
246 | |
247 | if (updateInfo.updateRectBounds.isEmpty()) |
248 | return; |
249 | |
250 | if (!m_backingStore) |
251 | m_backingStore = std::make_unique<BackingStore>(updateInfo.viewSize, updateInfo.deviceScaleFactor, m_webPageProxy); |
252 | |
253 | m_backingStore->incorporateUpdate(updateInfo); |
254 | |
255 | Region damageRegion; |
256 | if (updateInfo.scrollRect.isEmpty()) { |
257 | for (const auto& rect : updateInfo.updateRects) |
258 | damageRegion.unite(rect); |
259 | } else |
260 | damageRegion = IntRect(IntPoint(), m_webPageProxy.viewSize()); |
261 | m_webPageProxy.setViewNeedsDisplay(damageRegion); |
262 | } |
263 | #endif |
264 | |
265 | bool DrawingAreaProxyCoordinatedGraphics::alwaysUseCompositing() const |
266 | { |
267 | return m_webPageProxy.preferences().acceleratedCompositingEnabled() && m_webPageProxy.preferences().forceCompositingMode(); |
268 | } |
269 | |
270 | void DrawingAreaProxyCoordinatedGraphics::enterAcceleratedCompositingMode(const LayerTreeContext& layerTreeContext) |
271 | { |
272 | ASSERT(!isInAcceleratedCompositingMode()); |
273 | #if !PLATFORM(WPE) |
274 | m_backingStore = nullptr; |
275 | #endif |
276 | m_layerTreeContext = layerTreeContext; |
277 | m_webPageProxy.enterAcceleratedCompositingMode(layerTreeContext); |
278 | } |
279 | |
280 | void DrawingAreaProxyCoordinatedGraphics::exitAcceleratedCompositingMode() |
281 | { |
282 | ASSERT(isInAcceleratedCompositingMode()); |
283 | |
284 | m_layerTreeContext = { }; |
285 | m_webPageProxy.exitAcceleratedCompositingMode(); |
286 | } |
287 | |
288 | void DrawingAreaProxyCoordinatedGraphics::updateAcceleratedCompositingMode(const LayerTreeContext& layerTreeContext) |
289 | { |
290 | ASSERT(isInAcceleratedCompositingMode()); |
291 | |
292 | m_layerTreeContext = layerTreeContext; |
293 | m_webPageProxy.updateAcceleratedCompositingMode(layerTreeContext); |
294 | } |
295 | |
296 | void DrawingAreaProxyCoordinatedGraphics::backingStoreStateDidChange(RespondImmediatelyOrNot respondImmediatelyOrNot) |
297 | { |
298 | ++m_nextBackingStoreStateID; |
299 | sendUpdateBackingStoreState(respondImmediatelyOrNot); |
300 | } |
301 | |
302 | void DrawingAreaProxyCoordinatedGraphics::sendUpdateBackingStoreState(RespondImmediatelyOrNot respondImmediatelyOrNot) |
303 | { |
304 | ASSERT(m_currentBackingStoreStateID < m_nextBackingStoreStateID); |
305 | |
306 | if (!m_webPageProxy.hasRunningProcess()) |
307 | return; |
308 | |
309 | if (m_isWaitingForDidUpdateBackingStoreState) |
310 | return; |
311 | |
312 | if (m_webPageProxy.viewSize().isEmpty() && !m_webPageProxy.useFixedLayout()) |
313 | return; |
314 | |
315 | m_isWaitingForDidUpdateBackingStoreState = respondImmediatelyOrNot == RespondImmediately; |
316 | |
317 | send(Messages::DrawingArea::UpdateBackingStoreState(m_nextBackingStoreStateID, respondImmediatelyOrNot == RespondImmediately, m_webPageProxy.deviceScaleFactor(), m_size, m_scrollOffset)); |
318 | m_scrollOffset = IntSize(); |
319 | |
320 | if (m_isWaitingForDidUpdateBackingStoreState) { |
321 | // Start the responsiveness timer. We will stop it when we hear back from the WebProcess |
322 | // in didUpdateBackingStoreState. |
323 | process().responsivenessTimer().start(); |
324 | } |
325 | |
326 | if (m_isWaitingForDidUpdateBackingStoreState && !m_layerTreeContext.isEmpty()) { |
327 | // Wait for the DidUpdateBackingStoreState message. Normally we do this in DrawingAreaProxyCoordinatedGraphics::paint, but that |
328 | // function is never called when in accelerated compositing mode. |
329 | waitForAndDispatchDidUpdateBackingStoreState(); |
330 | } |
331 | } |
332 | |
333 | void DrawingAreaProxyCoordinatedGraphics::waitForAndDispatchDidUpdateBackingStoreState() |
334 | { |
335 | ASSERT(m_isWaitingForDidUpdateBackingStoreState); |
336 | |
337 | if (!m_webPageProxy.hasRunningProcess()) |
338 | return; |
339 | if (process().state() == WebProcessProxy::State::Launching) |
340 | return; |
341 | if (!m_webPageProxy.isViewVisible()) |
342 | return; |
343 | #if PLATFORM(WAYLAND) && USE(EGL) |
344 | // Never block the UI process in Wayland when waiting for DidUpdateBackingStoreState after a resize, |
345 | // because the nested compositor needs to handle the web process requests that happens while resizing. |
346 | if (PlatformDisplay::sharedDisplay().type() == PlatformDisplay::Type::Wayland && isInAcceleratedCompositingMode()) |
347 | return; |
348 | #endif |
349 | |
350 | // FIXME: waitForAndDispatchImmediately will always return the oldest DidUpdateBackingStoreState message that |
351 | // hasn't yet been processed. But it might be better to skip ahead to some other DidUpdateBackingStoreState |
352 | // message, if multiple DidUpdateBackingStoreState messages are waiting to be processed. For instance, we could |
353 | // choose the most recent one, or the one that is closest to our current size. |
354 | |
355 | // The timeout, in seconds, we use when waiting for a DidUpdateBackingStoreState message when we're asked to paint. |
356 | process().connection()->waitForAndDispatchImmediately<Messages::DrawingAreaProxy::DidUpdateBackingStoreState>(m_identifier.toUInt64(), Seconds::fromMilliseconds(500)); |
357 | } |
358 | |
359 | #if !PLATFORM(WPE) |
360 | void DrawingAreaProxyCoordinatedGraphics::discardBackingStoreSoon() |
361 | { |
362 | if (!m_backingStore || !m_isBackingStoreDiscardable || m_discardBackingStoreTimer.isActive()) |
363 | return; |
364 | |
365 | // We'll wait this many seconds after the last paint before throwing away our backing store to save memory. |
366 | // FIXME: It would be smarter to make this delay based on how expensive painting is. See <http://webkit.org/b/55733>. |
367 | static const Seconds discardBackingStoreDelay = 2_s; |
368 | |
369 | m_discardBackingStoreTimer.startOneShot(discardBackingStoreDelay); |
370 | } |
371 | |
372 | void DrawingAreaProxyCoordinatedGraphics::discardBackingStore() |
373 | { |
374 | if (!m_backingStore) |
375 | return; |
376 | m_backingStore = nullptr; |
377 | backingStoreStateDidChange(DoNotRespondImmediately); |
378 | } |
379 | #endif |
380 | |
381 | #if USE(TEXTURE_MAPPER_GL) && PLATFORM(GTK) && PLATFORM(X11) && !USE(REDIRECTED_XCOMPOSITE_WINDOW) |
382 | void DrawingAreaProxyCoordinatedGraphics::setNativeSurfaceHandleForCompositing(uint64_t handle) |
383 | { |
384 | if (!m_hasReceivedFirstUpdate) { |
385 | m_pendingNativeSurfaceHandleForCompositing = handle; |
386 | return; |
387 | } |
388 | send(Messages::DrawingArea::SetNativeSurfaceHandleForCompositing(handle), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply); |
389 | } |
390 | |
391 | void DrawingAreaProxyCoordinatedGraphics::destroyNativeSurfaceHandleForCompositing() |
392 | { |
393 | if (m_pendingNativeSurfaceHandleForCompositing) { |
394 | m_pendingNativeSurfaceHandleForCompositing = 0; |
395 | return; |
396 | } |
397 | bool handled; |
398 | sendSync(Messages::DrawingArea::DestroyNativeSurfaceHandleForCompositing(), Messages::DrawingArea::DestroyNativeSurfaceHandleForCompositing::Reply(handled)); |
399 | } |
400 | #endif |
401 | |
402 | DrawingAreaProxyCoordinatedGraphics::DrawingMonitor::DrawingMonitor(WebPageProxy& webPage) |
403 | : m_timer(RunLoop::main(), this, &DrawingMonitor::stop) |
404 | #if PLATFORM(GTK) |
405 | , m_webPage(webPage) |
406 | #endif |
407 | { |
408 | #if USE(GLIB_EVENT_LOOP) |
409 | #if PLATFORM(GTK) |
410 | // Give redraws more priority. |
411 | m_timer.setPriority(GDK_PRIORITY_REDRAW - 10); |
412 | #else |
413 | m_timer.setPriority(RunLoopSourcePriority::RunLoopDispatcher); |
414 | #endif |
415 | #endif |
416 | } |
417 | |
418 | DrawingAreaProxyCoordinatedGraphics::DrawingMonitor::~DrawingMonitor() |
419 | { |
420 | m_callback = nullptr; |
421 | stop(); |
422 | } |
423 | |
424 | int DrawingAreaProxyCoordinatedGraphics::DrawingMonitor::webViewDrawCallback(DrawingAreaProxyCoordinatedGraphics::DrawingMonitor* monitor) |
425 | { |
426 | monitor->didDraw(); |
427 | return FALSE; |
428 | } |
429 | |
430 | void DrawingAreaProxyCoordinatedGraphics::DrawingMonitor::start(WTF::Function<void(CallbackBase::Error)>&& callback) |
431 | { |
432 | m_startTime = MonotonicTime::now(); |
433 | m_callback = WTFMove(callback); |
434 | #if PLATFORM(GTK) |
435 | g_signal_connect_swapped(m_webPage.viewWidget(), "draw" , reinterpret_cast<GCallback>(webViewDrawCallback), this); |
436 | m_timer.startOneShot(1_s); |
437 | #else |
438 | m_timer.startOneShot(0_s); |
439 | #endif |
440 | } |
441 | |
442 | void DrawingAreaProxyCoordinatedGraphics::DrawingMonitor::stop() |
443 | { |
444 | m_timer.stop(); |
445 | #if PLATFORM(GTK) |
446 | g_signal_handlers_disconnect_by_func(m_webPage.viewWidget(), reinterpret_cast<gpointer>(webViewDrawCallback), this); |
447 | #endif |
448 | m_startTime = MonotonicTime(); |
449 | if (m_callback) { |
450 | m_callback(CallbackBase::Error::None); |
451 | m_callback = nullptr; |
452 | } |
453 | } |
454 | |
455 | void DrawingAreaProxyCoordinatedGraphics::DrawingMonitor::didDraw() |
456 | { |
457 | // We wait up to 1 second for draw events. If there are several draw events queued quickly, |
458 | // we want to wait until all of them have been processed, so after receiving a draw, we wait |
459 | // up to 100ms for the next one or stop. |
460 | if (MonotonicTime::now() - m_startTime > 1_s) |
461 | stop(); |
462 | else |
463 | m_timer.startOneShot(100_ms); |
464 | } |
465 | |
466 | void DrawingAreaProxyCoordinatedGraphics::dispatchAfterEnsuringDrawing(WTF::Function<void(CallbackBase::Error)>&& callbackFunction) |
467 | { |
468 | if (!m_webPageProxy.hasRunningProcess()) { |
469 | callbackFunction(CallbackBase::Error::OwnerWasInvalidated); |
470 | return; |
471 | } |
472 | |
473 | if (!m_drawingMonitor) |
474 | m_drawingMonitor = std::make_unique<DrawingAreaProxyCoordinatedGraphics::DrawingMonitor>(m_webPageProxy); |
475 | m_drawingMonitor->start(WTFMove(callbackFunction)); |
476 | } |
477 | |
478 | } // namespace WebKit |
479 | |