1. Introduction
User latency is an important quality benchmark for Web Applications.
While JavaScript-based mechanisms can provide comprehensive
instrumentation for user latency measurements within an application, in
many cases, they are unable to provide a complete end-to-end latency
picture. This document introduces the PerformanceResourceTiming
interface to allow JavaScript mechanisms to collect complete timing
information related to resources on a document. Navigation Timing 2
[NAVIGATION-TIMING-2] extends this specification to provide
additional timing information associated with a navigation.
For example, the following JavaScript shows a simple attempt to measure the time it takes to fetch a resource:
<!doctype html> < html > < head > </ head > < body onload = "loadResources()" > < script > function loadResources() { var start= new Date(). getTime(); var image1= new Image(); var resourceTiming= function () { var now= new Date(). getTime(); var latency= now- start; alert( "End to end resource fetch: " + latency); }; image1. onload= resourceTiming; image1. src= 'https://www.w3.org/Icons/w3c_main.png' ; } </ script > < img src = "https://www.w3.org/Icons/w3c_home.png" > </ body > </ html >
Though this script can measure the time it takes to fetch a resource, it cannot break down the time spent in various phases. Further, the script cannot easily measure the time it takes to fetch resources described in markup.
To address the need for complete information on user experience, this
document introduces the PerformanceResourceTiming interface.
This interface allows JavaScript mechanisms to provide complete
client-side latency measurements within applications. With this
interface, the previous example can be modified to measure a user’s
perceived load time of a resource.
The following script calculates the amount of time it takes to fetch
every resource in the page, even those defined in markup. This example
assumes that this page is hosted on https://www.w3.org. One could
further measure the amount of time it takes in every phase of fetching
a resource with the PerformanceResourceTiming interface.
<!doctype html> < html > < head > </ head > < body onload = "loadResources()" > < script > function loadResources() { var image1= new Image(); image1. onload= resourceTiming; image1. src= 'https://www.w3.org/Icons/w3c_main.png' ; } function resourceTiming() { var resourceList= window. performance. getEntriesByType( "resource" ); for ( i= 0 ; i< resourceList. length; i++ ) { if ( resourceList[ i]. initiatorType== "img" ) { alert( "End to end resource fetch: " + ( resourceList[ i]. responseEnd- resourceList[ i]. startTime)); } } } </ script > < img id = "image0" src = "https://www.w3.org/Icons/w3c_home.png" > </ body > </ html >
2. Terminology
The construction "a Foo object", where Foo is
actually an interface, is sometimes used instead of the more accurate
"an object implementing the interface Foo.
Throughout this work, all time values are measured in milliseconds since the start of navigation of the document [HR-TIME]. For example, the start of navigation of the document occurs at time 0.
This definition of time is based on the High Resolution Time specification [HR-TIME] and is different from the definition of time used in the Navigation Timing specification [NAVIGATION-TIMING-2], where time is measured in milliseconds since midnight of January 1, 1970 (UTC).
3. Resource Timing
3.1. Introduction
The PerformanceResourceTiming interface facilitates timing
measurement of fetched http(s)
resources. For example, this interface is available for
XMLHttpRequest objects [XHR], HTML elements [HTML] such as
iframe, img, script, object, embed and link
with the link type of stylesheet, SVG elements [SVG11]
such as svg, and
EventSource.
3.2.
Resources Included in the PerformanceResourceTiming Interface
This section is non-normative.
Resource Requests fetched by a non-null client
are included as PerformanceResourceTiming objects in the
client’s global object’s
Performance Timeline, unless excluded from the timeline as
part of the fetching process. Resources that are retrieved
from HTTP cache are included as PerformanceResourceTiming
objects in the Performance Timeline. Resources for which the
fetch was initiated, but was later aborted (e.g. due to a network
error) are included as PerformanceResourceTiming objects in
the Performance Timeline, with their start and end timing.
Examples:
- If the same canonical URL is used as the
srcattribute of two HTMLIMGelements, the fetch of the resource initiated by the first HTMLIMGelement would be included as aPerformanceResourceTimingobject in the Performance Timeline. The user agent might not re-request the URL for the second HTMLIMGelement, instead using the existing download it initiated for the first HTMLIMGelement. In this case, the fetch of the resource by the firstIMGelement would be the only occurrence in the Performance Timeline. - If the
srcattribute of a HTMLIMGelement is changed via script, both the fetch of the original resource as well as the fetch of the new URL would be included asPerformanceResourceTimingobjects in the Performance Timeline. - If an HTML
IFRAMEelement is added via markup without specifying asrcattribute, the user agent may load theabout:blankdocument for theIFRAME. If at a later time thesrcattribute is changed dynamically via script, the user agent may fetch the new URL resource for theIFRAME. In this case, only the fetch of the new URL would be included as aPerformanceResourceTimingobject in the Performance Timeline. - If an
XMLHttpRequestis generated twice for the same canonical URL, both fetches of the resource would be included as aPerformanceResourceTimingobject in the Performance Timeline. This is because the fetch of the resource for the secondXMLHttpRequestcannot reuse the download issued for the firstXMLHttpRequest. - If an HTML
IFRAMEelement is included on the page, then only the resource requested byIFRAMEsrcattribute is included as aPerformanceResourceTimingobject in the Performance Timeline. Sub-resources requested by theIFRAMEdocument will be included in theIFRAMEdocument’s Performance Timeline and not the parent document’s Performance Timeline. - If an HTML
IMGelement has adata: URIas its source [RFC2397], then this resource will not be included as aPerformanceResourceTimingobject in the Performance Timeline.PerformanceResourceTimingentries are only reported for http(s) resources. - If a resource fetch was
aborted due to a networking error (e.g. DNS, TCP, or TLS error), then
the fetch will be included as a
PerformanceResourceTimingobject in the Performance Timeline with only thestartTime,fetchStart,durationandresponseEndset. - If a resource fetch is
aborted because it failed a fetch precondition (e.g. mixed content,
CORS restriction, CSP policy, etc), then this resource will not be
included as a
PerformanceResourceTimingobject in the Performance Timeline.
3.3. The PerformanceResourceTiming Interface
[Exposed =(Window ,Worker )]interface :PerformanceResourceTiming PerformanceEntry {readonly attribute DOMString ;initiatorType readonly attribute DOMString ;deliveryType readonly attribute ByteString ;nextHopProtocol readonly attribute DOMHighResTimeStamp ;workerStart readonly attribute DOMHighResTimeStamp ;redirectStart readonly attribute DOMHighResTimeStamp ;redirectEnd readonly attribute DOMHighResTimeStamp ;fetchStart readonly attribute DOMHighResTimeStamp ;domainLookupStart readonly attribute DOMHighResTimeStamp ;domainLookupEnd readonly attribute DOMHighResTimeStamp ;connectStart readonly attribute DOMHighResTimeStamp ;connectEnd readonly attribute DOMHighResTimeStamp ;secureConnectionStart readonly attribute DOMHighResTimeStamp ;requestStart readonly attribute DOMHighResTimeStamp ;finalResponseHeadersStart readonly attribute DOMHighResTimeStamp ;firstInterimResponseStart readonly attribute DOMHighResTimeStamp ;responseStart readonly attribute DOMHighResTimeStamp ;responseEnd readonly attribute DOMHighResTimeStamp ;workerRouterEvaluationStart readonly attribute DOMHighResTimeStamp ;workerCacheLookupStart readonly attribute DOMString ;workerMatchedRouterSource readonly attribute DOMString ;workerFinalRouterSource readonly attribute unsigned long long ;transferSize readonly attribute unsigned long long ;encodedBodySize readonly attribute unsigned long long ;decodedBodySize readonly attribute unsigned short ;responseStatus readonly attribute RenderBlockingStatusType ;renderBlockingStatus readonly attribute DOMString ;contentType readonly attribute DOMString ; [contentEncoding Default ]object (); };toJSON
A PerformanceResourceTiming has an associated DOMString
initiator
type.
A PerformanceResourceTiming has an associated DOMString
delivery
type.
A PerformanceResourceTiming has an associated DOMString
requested
URL.
A PerformanceResourceTiming has an associated DOMString
cache mode
(the empty string, "local", or
"validated").
A PerformanceResourceTiming has an associated fetch timing info timing
info.
A PerformanceResourceTiming has an associated response body info resource
info.
A PerformanceResourceTiming has an associated
status
response status.
A PerformanceResourceTiming has an associated
RenderBlockingStatusType render-blocking status.
When toJSON is called, run the default toJSON steps
for PerformanceResourceTiming.
initiatorType getter steps are to return the initiator type for this.
initiatorType returns one of the following values:
-
"navigation", if the request is a navigation request; -
"body", if the request is a result of processing thebodyelement’sbackgroundattribute that’s already obsolete. -
"css", if the request is a result of processing a CSS url() directive such as@import url()orbackground: url(); [CSS-VALUES]Note: the request for a font resource specified with
@font-facein CSS is a result of processing a CSS directive. Therefore, theinitiatorTypefor this font resource is"css". -
"script", if the request is a result of loading any script (a classicscript, a module script, or aWorker). -
"xmlhttprequest", if the request is a result of processing anXMLHttpRequest; -
"font", if the request is the result of processing fonts. This can happen when fonts request subsequent resources, e.g, when Incremental Font Transfer [INCREMENTAL_FONT_TRANSFER] is used. -
"fetch", if the request is the result of processing thefetch()method; -
"beacon", if the request is the result of processing thesendBeacon()method; [BEACON] -
"video", if the request is the result of processing thevideoelement’sposterorsrc. -
"audio", if the request is the result of processing theaudioelement’ssrc. -
"track", if the request is the result of processing thetrackelement’ssrc. -
"img", if the request is the result of processing theimgelement’ssrcorsrcset. -
"image", if the request is the result of processing the image element. [SVG2] -
"input", if the request is the result of processing aninputelement oftypeimage. -
"ping", if the request is the result of processing anaelement’sping. -
"iframe", if the request is the result of processing aniframe’ssrc. -
"frame", if the request is the result of loading aframe. -
"embed", if the request is the result of processing anembedelement’ssrc. -
"link", if the request is the result of processing anlinkelement. -
"object", if the request is the result of processing anobjectelement. -
"early-hints", if the request is the result of processing an Early Hints [EARLY_HINTS] response. -
"other", if none of the above conditions match.
The setting of initiatorType is done at the different places where
a resource timing entry is reported, such as the fetch standard.
deliveryType getter steps are to return the delivery type for this.
deliveryType returns one of the following values:
-
"cache", if the cache mode is not the empty string. - the empty string
"", if none of the above conditions match.
This is expected to be expanded by future updates to this specification, e.g. to describe consuming preloaded resources and prefetched navigation requests.
The workerStart getter steps are to convert fetch timestamp for this’s timing info’s final service worker start time and the relevant global object for this. See HTTP fetch for more info.
The redirectStart getter steps are to convert fetch timestamp for this’s timing info’s redirect start time and the relevant global object for this. See HTTP-redirect fetch for more info.
The redirectEnd getter steps are to convert fetch timestamp for this’s timing info’s redirect end time and the relevant global object for this. See HTTP-redirect fetch for more info.
The fetchStart getter steps are to convert fetch timestamp for this’s timing info’s post-redirect start time and the relevant global object for this. See HTTP fetch for more info.
The domainLookupStart getter steps are to convert fetch timestamp for this’s timing info’s final connection timing info’s domain lookup start time and the relevant global object for this. See Recording connection timing info for more info.
The domainLookupEnd getter steps are to convert fetch timestamp for this’s timing info’s final connection timing info’s domain lookup end time and the relevant global object for this. See Recording connection timing info for more info.
The connectStart getter steps are to convert fetch timestamp for this’s timing info’s final connection timing info’s connection start time and the relevant global object for this. See Recording connection timing info for more info.
The connectEnd getter steps are to convert fetch timestamp for this’s timing info’s final connection timing info’s connection end time and the relevant global object for this. See Recording connection timing info for more info.
The secureConnectionStart getter steps are to convert fetch timestamp for this’s timing info’s final connection timing info’s secure connection start time and the relevant global object for this. See Recording connection timing info for more info.
The nextHopProtocol getter steps are to isomorphic decode this’s timing info’s final connection timing info’s ALPN negotiated protocol. See Recording connection timing info for more info.
Issue 221 suggests to remove support for nextHopProtocol, as it can reveal details about the user’s network configuration.
The requestStart getter steps are to convert fetch timestamp for this’s timing info’s final network-request start time and the relevant global object for this. See HTTP fetch for more info.
The firstInterimResponseStart getter steps are to convert fetch timestamp for this’s timing info’s first interim network-response start time and the relevant global object for this. See HTTP fetch for more info.
The finalResponseHeadersStart getter steps are to convert fetch timestamp for this’s timing info’s final network-response start time and the relevant global object for this. See HTTP fetch for more info.
The responseStart getter steps are to return this’s
firstInterimResponseStart if it is not
0; Otherwise this’s
finalResponseHeadersStart.
The responseEnd getter steps are to convert fetch timestamp for this’s timing info’s end time and the relevant global object for this. See fetch for more info.
The encodedBodySize getter steps are to return this’s resource info’s encoded size.
The decodedBodySize getter steps are to return this’s resource info’s decoded size.
The transferSize getter steps are:
-
If this’s cache mode is "
local", then return 0. -
If this’s cache mode is "
validated", then return 300. -
Return this’s resource info’s encoded size plus 300.
The constant number added to
transferSizereplaces exposing the total byte size of the HTTP headers, as that might expose the presence of certain cookies. See this issue.
The responseStatus getter steps are to return this’s response status.
responseStatus is determined in Fetch. For a cross-origin
no-cors request it
would be 0 because the response would be an opaque filtered
response.
The contentType getter steps are to return this’s resource info’s content type.
The contentEncoding getter steps are to return this’s resource info’s content encoding.
The renderBlockingStatus getter steps are to return blocking if this’s timing info’s render-blocking is true; otherwise non-blocking.
The workerRouterEvaluationStart getter steps are to return this’s timing info’s service worker timing info’s worker router evaluation start.
The workerCacheLookupStart getter steps are to return this’s timing info’s service worker timing info’s worker cache lookup start.
The workerMatchedRouterSource getter steps are to return this’s timing info’s service worker timing info’s worker matched router source.
The workerFinalRouterSource getter steps are to return this’s timing info’s service worker timing info’s worker final router source.
A user agent implementing PerformanceResourceTiming would need
to include "resource" in
supportedEntryTypes. This allows developers
to detect support for Resource Timing.
3.3.1. RenderBlockingStatusType enum
enum {RenderBlockingStatusType ,"blocking" };"non-blocking"
The values are defined as follows:
- blocking
- The resource can potentially block rendering.
- non-blocking
- The resource will not block rendering.
3.4.
Extensions to the Performance Interface
The user agent MAY choose to limit how many resources are included as
PerformanceResourceTiming objects in the Performance
Timeline [PERFORMANCE-TIMELINE-2]. This section extends the
Performance
interface to allow controls over the number of
PerformanceResourceTiming objects stored.
The recommended minimum number of PerformanceResourceTiming
objects is 250, though this may be changed by the user agent.
setResourceTimingBufferSize can be
called to request a change to this limit.
Each ECMAScript global environment has:
- A resource timing buffer size limit which should initially be 250 or greater.
- A resource timing buffer current size which is initially 0.
- A resource timing buffer full event pending flag which is initially false.
- A resource timing secondary buffer current size which is initially 0.
- A resource timing secondary buffer to store
PerformanceResourceTimingobjects that is initially empty.
partial interface Performance {undefined ();clearResourceTimings undefined (setResourceTimingBufferSize unsigned long );maxSize attribute EventHandler ; };onresourcetimingbufferfull
The Performance interface is defined in [HR-TIME].
The method clearResourceTimings runs the following steps:
- Remove all
PerformanceResourceTimingobjects in the performance entry buffer. - Set resource timing buffer current size to 0.
The setResourceTimingBufferSize method runs the following steps:
- Set resource timing buffer size limit to the
maxSize parameter. If the maxSize parameter is less
than resource timing buffer current size, no
PerformanceResourceTimingobjects are to be removed from the performance entry buffer.
The attribute onresourcetimingbufferfull is the event
handler for the resourcetimingbufferfull event described
below.
To check if can add resource timing entry, run the following steps:
- If resource timing buffer current size is smaller than resource timing buffer size limit, return true.
- Return false.
To add a PerformanceResourceTiming entry new entry into the performance entry buffer, run the following steps:
-
If can add resource timing entry returns true and
resource timing buffer full event pending flag is false, run
the following substeps:
- Add new entry to the performance entry buffer.
- Increase resource timing buffer current size by 1.
- Return.
-
If resource timing buffer full event pending flag is
false, run the following substeps:
- Set resource timing buffer full event pending flag to true.
- Queue a task on the performance timeline task source to run fire a buffer full event.
- Add new entry to the resource timing secondary buffer.
- Increase resource timing secondary buffer current size by 1.
To copy secondary buffer, run the following steps:
-
While resource timing secondary buffer is not empty and
can add resource timing entry returns true, run the following
substeps:
- Let entry be the oldest
PerformanceResourceTimingin resource timing secondary buffer. - Add entry to the end of performance entry buffer.
- Increment resource timing buffer current size by 1.
- Remove entry from resource timing secondary buffer.
- Decrement resource timing secondary buffer current size by 1.
- Let entry be the oldest
To fire a buffer full event, run the following steps:
-
While resource timing secondary buffer is not empty, run
the following substeps:
- Let number of excess entries before be resource timing secondary buffer current size.
- If can add resource timing entry returns false, then
fire an event named
resourcetimingbufferfullat thePerformanceobject. - Run copy secondary buffer.
- Let number of excess entries after be resource timing secondary buffer current size.
- If number of excess entries before is lower than or equals number of excess entries after, then remove all entries from resource timing secondary buffer, set resource timing secondary buffer current size to 0, and abort these steps.
-
Set resource timing buffer full event pending flag to
false.
This means that if the
resourcetimingbufferfullevent handler does not add more room in the buffer than it adds resources to it, excess entries will be dropped from the buffer. Developers need to make sure thatresourcetimingbufferfullevent handlers callclearResourceTimingsor extend the buffer sufficiently (by callingsetResourceTimingBufferSize).
3.5. Cross-origin Resources
3.5.1. Introduction
As detailed in Fetch, requests for cross-origin resources are
included as PerformanceResourceTiming objects in the
Performance Timeline.
If the timing
allow check algorithm fails for a cross-origin resource, the
entry will be an opaque entry. Such
entries have most of their attributes masked in order to prevent
leaking cross-origin data that isn’t otherwise exposed. So, for an
opaque entry, the following
attributes will always return zero or the empty string:
redirectStart,
redirectEnd,
workerStart,
domainLookupStart,
domainLookupEnd,
connectStart,
connectEnd,
requestStart,
firstInterimResponseStart,
finalResponseHeadersStart,
responseStart,
secureConnectionStart, and
nextHopProtocol.
Some of the properties, like contentType,
encodedBodySize, and
decodedBodySize are set to zero (or
the empty string in the case of
contentType) when the response is
CORS-cross-origin.
transferSize is affected both by the
timing
allow check and by the
CORS-cross-origin
status.
For requests handled by a service worker using
respondWith(), the reported timing data reflects the
interaction between the client and the service worker, rather than
the service worker’s own internal network activity. For example, the
service worker might respond to a same-origin request with a
cross-origin response or vice versa, or return a cached or synthetic
response to either. Given that, resources forwarded from a service
worker do not tell the whole story of fetching the resource, and do
not go through the timing
allow check. To get the full information about those fetches,
the service worker’s own performance timeline can be inspected.
[SERVICE-WORKERS]
For more details, see HTTP Fetch
#4 - the timing
allow check is only performed when there is no response from the
service worker. In addition, the response cloned in the
respondWith() algorithm does not carry the fetch timing info of the internal fetch, as that information is attached
to a fetch rather than to a response.
3.5.2.
Timing-Allow-Origin Response Header
Server-side applications may return the Timing-Allow-Origin HTTP response header to allow the User Agent to fully expose, to the document origin(s) specified, the values of attributes that would have been zero due to those cross-origin restrictions.
The Timing-Allow-Origin HTTP response header field can be used to communicate a policy indicating origin(s) that may be allowed to see values of attributes that would have been zero due to the cross-origin restrictions. The header’s value is represented by the following ABNF [RFC5234] (using List Extension, [RFC9110]):
Timing-Allow-Origin = 1#( origin-or-null / wildcard )
The sender MAY generate multiple Timing-Allow-Origin header fields. The recipient MAY combine multiple Timing-Allow-Origin header fields by appending each subsequent field value to the combined field value in order, separated by a comma.
The user agent MAY still enforce cross-origin restrictions and set transferSize, encodedBodySize, and decodedBodySize attributes to zero, even with Timing-Allow-Origin HTTP response header fields. If it does, it MAY also set deliveryType to "".
The Timing-Allow-Origin headers are processed in FETCH to compute the attributes accordingly.
The Timing-Allow-Origin header might arrive as part of a cached response. In case of cache revalidation, according to RFC 7234, the header’s value might come from the revalidation response, or if not present there, from the original cached resource.
Issues 222 and 223 suggest to remove wildcard support from Timing-Allow-Origin in order to restrict its use.
3.5.3. IANA Considerations
This section registers Timing-Allow-Origin as a Provisional Message Header.
- Header field name:
-
Timing-Allow-Origin
- Applicable protocol:
- http
- Status:
- provisional
- Author/Change controller:
- W3C
- Specification document:
- § 3.5.2 Timing-Allow-Origin Response Header
3.6. Resource Timing Attributes
This section is non-normative.
The following graph illustrates the timing attributes defined by the PerformanceResourceTiming interface. Attributes in parenthesis may not be available when fetching cross-origin resources. User agents may perform internal processing in between timings, which allow for non-normative intervals between timings.
PerformanceResourceTiming interface. Attributes in
parenthesis indicate that they may not be available if the resource
fails the timing allow
check algorithm.
4. Creating a resource timing entry
To mark resource timing given a fetch timing info timingInfo, a DOMString requestedURL, a DOMString initiatorType a global object global, a string cacheMode, a response body info bodyInfo, a status responseStatus, and an optional string deliveryType (by default, the empty string), perform the following steps:
- Create a
PerformanceResourceTimingobject entry in global’s realm. - Setup the resource timing entry for entry, given initiatorType, requestedURL, timingInfo, cacheMode, bodyInfo, responseStatus, and deliveryType.
- Queue a PerformanceEntry entry.
- Add entry to global’s performance entry buffer.
To setup the resource timing entry for
PerformanceResourceTiming entry given DOMString
initiatorType, DOMString requestedURL, fetch timing info
timingInfo, a DOMString cacheMode, a response body info
bodyInfo, a status
responseStatus, and an optional DOMString deliveryType (by
default, the empty string), perform the following steps:
- Assert that cacheMode is the empty string,
"
local", or "validated". - Let global be entry’s relevant global object.
- Initialize entry given the
result of converting
timingInfo’s start time given global,
"
resource", requestedURL, and the result of converting timingInfo’s end time given global. - Set entry’s initiator type to initiatorType.
- Set entry’s requested URL to requestedURL.
- Set entry’s timing info to timingInfo.
- Set entry’s resource info to bodyInfo.
- Set entry’s cache mode to cacheMode.
- Set entry’s response status to responseStatus.
- If deliveryType is the empty string and cacheMode is not,
then set deliveryType to "
cache". - Set entry’s delivery type to deliveryType.
To convert fetch timestamp given DOMHighResTimeStamp
ts and global object global, do the following:
- If ts is zero, return zero.
- Otherwise, return the relative high resolution coarse time given ts and global.
5. Security Considerations
The PerformanceResourceTiming interface exposes timing
information for a resource to any web page or worker that has
requested that resource. To limit the access to the
PerformanceResourceTiming interface, the
same origin
policy is enforced by default and certain attributes are set to zero,
as described in HTTP fetch. Resource providers can explicitly
allow all timing information to be collected for a resource by adding
the Timing-Allow-Origin HTTP response header, which specifies
the domains that are allowed to access the timing information.
6. Privacy Considerations
Statistical fingerprinting is a privacy concern where a malicious web
site might determine whether a user has visited a third-party web site
by measuring the timing of cache hits and misses of resources in the
third-party web site. Though the PerformanceResourceTiming
interface gives timing information for resources in a document, the
load event on resources can already measure timing to determine cache
hits and misses in a limited fashion, and the cross-origin
restrictions in HTTP Fetch prevent the leakage of any additional
information.
7. Acknowledgments
Thanks to Anne Van Kesteren, Annie Sullivan, Arvind Jain, Boris Zbarsky, Darin Fisher, Jason Weber, Jonas Sicking, James Simonsen, Karen Anderson, Kyle Scholz, Nic Jansma, Philippe Le Hegaret, Sigbjørn Vik, Steve Souders, Todd Reifsteck, Tony Gentilcore, William Chan, and Alex Christensen for their contributions to this work.