
Measuring Performance Using Logging
Learn how to use signposts and logging to measure performance. Understand how the Points of Interest instrument can be used to examine logged data. Get an introduction into creating and using custom instruments.
Resources
Related Videos
WWDC23
WWDC18
Good afternoon.
My name is Shane.I'm with the Darwin Runtimeteam. And I'd like to welcome you tomeasuring performance usinglogging.
So we heard a lot aboutperformance on Monday.
Performance is one of thosethings that's key to a greatuser experience.People love it when their gamesand their apps are fast,dynamic, and responsive.
But software is complex so thatmeans that when your app istrying to do something,sometimes a ton of things can begoing on and that means you canfind some performance wins insome pretty unlikely places.
But doing so, unearthing thoseof performance wins requires anunderstanding, sometimes a deepunderstanding of what it is yourprogram is doing.
It requires you to know whenyour code is executing exactly,how long a particular operationis taking.
So this is one place where agood tool can make a realdifference.
And we know that building bettertools, making them available toyou, is one of the ways that wecan help you be a moreproductive developer.
So today I'm going to talk aboutone of those tools.Today, I'm going to talk aboutsignposts.Signposts are a new member ofthe OSLog family.
And we're making them availableto you in macOS.We're making them available toyou in iOs.And you can use them in Swiftand in C, but the coolest thingis we've integrated them withInstruments.So that means Instruments cantake the data that signpostsproduce and give you a deepunderstanding of what it is yourprogram is doing.
So first a little history.
We introduced OSLog a couple ofyears ago.
It's our modern take on alogging facility.It's our way of gettingdebugging information out of thesystem.And it was built with our goalsof efficiency and privacy inmind.
Here you can see an example ofOSLog code where I've justcreated a simple log handle andposted a hello world to it.
Signposts extend the OSLog API,but they do it for theperformance use case.And that means they areconveying performance relatedinformation, and they'reintegrated with our developertools and that means you canannotate your code withsignposts and then pull upInstruments and see somethinglike this.
So Instruments is showing youthis beautiful visualization ofa timeline of what your programis doing and the signpostactivity there.And then on the bottom there'sthat table with statisticalaggregation and analysis of thesignpost data, slicing anddicing to see what yourprogram's behavior is reallylike.
In this session, I'll talk aboutadopting signposts into yourcode and show you some of whatthey're capable of.And then we're going todemonstrate the new Instrumentsignpost visualization to giveyou an idea how signposts andInstruments work together.So let's start.
I'm going to start with a reallybasic example.
Imagine that this is your app.
And what you're trying toinvestigate is the amount oftime a particular part of theinterface takes to refresh.And you know to do that you wantto load some images and put themon the screen.So once again, an abstract,simple view of this app might bethat you're doing the work tograb an asset.And after you've gotten themall, the interface is refreshed.
What a signpost allows us to dois to mark the beginning and theend of a piece of work and thenassociate those two points intime, those two log events witheach other.
And they do it with an ossignpost function call.There are two calls.One with .begin and one with.end. Here I've represented the beginwith that arrow with the bunderneath it.And I represented the end withthe arrow with the e under it.And then we're going to relatethose two points to each otherto give you a sense of what theelapsed time is for thatinterval. All right.
In code, there's this simpleimplementation of that algorithmwhere for each element in ourinterface, we're going to fetchthat asset and that's the pieceof operation that we'reinterested in measuring.
So to incorporate signpost intothis code base, we're going tosimply import the moduleos.signpost that contains thatfunctionality.
And then because signposts arepart of the OSLog functionality,we're going to create a loghandle.Here, this log handle takes twoarguments, a subsystem and acategory.
The subsystem is just probablythe same throughout yourproject.It looks a lot like your bundleID. And it represents the componentor the piece of software, maybethe framework that you'reworking on.
The category is used to relate-- to group related operationstogether or related signposts.And you'll see why that could beuseful later in the session.Once we have that log handle,we're just going to make twocalls to os signpost.One with .begin.One with .end.We're going to pass that loghandle into those calls.And then for the third argument,we have a signpost name.
The signpost name is a stringliteral that identifies theinterval that identifies theoperation that we're interestedin measuring.
That string literal is used tomatch up the begin point thatwe've annotated or that getsmarked up with that os signpostbegin called and the end point.So on our timeline, it justlooks like this.At the beginning of each pieceof work, we've dropped an ossignpost.At the end of each piece ofwork, we've dropped an ossignpost.And because those stringliterals at the begin and endcall sites line up with eachother, we can match those twotogether. But what if we're interested inalso measuring the entire amountof time the whole operation,that whole refresh took?Well, in our code, we're justgoing to add another pair of ossignpost begin and end calls.Pretty simple.And this time I've given it adifferent string literal, so adifferent signpost name.This time refresh panel toindicate that this is a separateinterval, separate from the oneinside the loop.
In our timeline, we're justmarking two additionalsignposts.
And that matching string literalof refresh panel will let thesystem know that those twopoints are associated with eachother.
All right.
It's not a super simple example.If your program ever does stepone and then step two then stepthree in a sequential fashionthen that would work.But in our systems, often wehave a lot of work that happensasynchronously.Right. So instead of having step one,step two, step three, we'reoften kicking things off insequence, right, and thenletting them complete later.So that means that theseoperations can happenconcurrently.They can overlap.
In that case, we need to givesome additional piece ofinformation to the system inorder for it to tell thosesignposts apart from each other.
And to do that, so far we'veonly used that name.Right. That name will match up the endand the beginning point.
So that string literal so farhas identified intervals, but ithasn't given us a way todiscriminate between overlappingintervals.
To do that, we're going to addanother piece of data to oursignpost calls called a signpostID.
The signpost ID will tell thesystem that these are the samekind of operation but each oneis different from each other.
So if two operations overlap butthey have different signpostIDs, the system will know thatthey're two different intervals.As long as you pass the samesignpost ID at the begin callsite and the end call site,those two signposts will beassociated with each other.
You can make signpost IDs withthis constructor here that takesa log handle, but you can alsomake them with an object.
This could be useful if you havesome object that represents thework that you're trying to doand the same signpost ID will begenerated as long as you use thesame instance of that object.So this means you don't have tocarry or store the signpost IDaround. You can just use the objectthat's handy.
Visually, you can think ofsignpost IDs as allowing us topass a little bit of extracontext to each signpost callwhich can relate the begin andend markers for a particularoperation with each other.
And this is important becausenot only can these operationsoverlap, but they often takediffering amounts of time.Let's see this in our codeexample.
So here's our code.I'm going to transform thatsynchronous fetch async call into an asynchronous one.
So here I'm just going to giveit a completion handler.This is a closure that will runafter the fetch asset iscomplete.
And then I've also added aclosure, a completion handlerfor running after all the assetshave been fetched.
In each case, I've moved that ossignpost end call inside of aclosure to indicate that that'swhen I want that marked periodof time to end.
Okay.
So because we think that theseintervals will overlap with eachother, we're going to create newsignpost IDs for each of them.Notice in the top example I'vecreated one with the constructortaking a log handle.And the second one, I've madeoff that object that is beingworked on, the element.And then I simply pass thosesignpost IDs into the call sitesand we're done.
You can think of signpost asbeing organized as a kind ofclassification or hierarchy.Right.All these operations are relatedtogether by the log handlemeaning that log category.
And then for each operation thatwe're interested in, we've givenit a signpost name.
Then because those signpostscould overlap with each other,we've given them that signpostID that tells the system thatthat's a specific instance ofthat interval.
This interface was builtspecifically to be flexible soyou control all the argumentsinto your begin site and yourend site.You control that signpost name,the log handle you give it, andthe ID.We've done this because as longas you can give the samearguments at the begin site andthe end site, those twosignposts will get matched witheach other.That means your begin and endsites can be in separatefunctions.
They can be associated withseparate objects.They may even live in separatesource files.
We've done this because we wantyou to be able to adopt it intoyour code base.And so whatever entry and exitconventions you have, you canuse these calls.So that's how to measureintervals with signposts.You may want to convey someadditional information, someadditional performance relevantinformation along with yoursignposts.And for that, we have a way toadd metadata to signpost calls.
So here's your basic signpostcall.To that, we can add anadditional string literalparameter.
This allows you to add somecontext to your begin and endcall sites.
Perhaps you have multiple beginand exit points for a particularoperation, but the stringliteral is also an OSLog formatstring.And that means I can use it topass additional data into thesignpost.So here, for example, I've usedthat %d to pass in fourintegers.
But because it's an OSLog formatstring, I can also use it topass many arguments of differenttypes. So here I've passed in somefloating-point numbers.And I've even used the formatspecifier to tell the system howmuch precision I want.You can pass dynamic strings inwith the string literalformatter.
And that'll let us pass ininformation that comes from afunction call or comes from auser entered piece ofinformation.
And we reference that formatstring literal with a fixedamount of storage which meansthat you can feel free to makeit as long and as human readableas you like.
This human readable string isthe same one that will berendered up in the Instruments.So you can feel free to give itsome context. I've given it here for thevarious arguments.And Instruments will be able toshow that full rendered string,or it still has programmaticaccess to the data that'sattached.
In addition to metadata forthose intervals, you may want toadd individual points in time.
That is, in addition to thebegin signpost and the endsignpost, you may have asignpost that's not tethered toa particular time interval butrather just some fixed moment.And for that, we have an ossignpost with the event type.
The os signpost with the eventtype call looks just like thesame as the begin and end, thistime with the event type.
And it marks a single point intime.
You could use this within thecontext of an interval or maybebecause you want to tracksomething that's independent ofan interval like a userinteraction.
So for that fetch asset intervalwe're talking about, maybe youwant to know when you'veconnected to the service thatprovides that asset.Or maybe you want to know when you've received a few bytes ofit.
You can use this to update thestatus or progress of aparticular interval many timesthroughout that time of thatinterval.
Or you might be tracking maybe atriggering event like maybe auser interface interaction likesomebody has just swiped toupdate that interface.Although, if you're reallyinvestigating in a performanceproblem, they might be swiping alot so this might be what yousee instead.
If you have signpost enabled,they're usually on by default,but I'd like to talk aboutconditionally turning them onand off.
First I'd like to emphasize thatwe built signpost to belightweight.That means we've done a lot ofwork to optimize them at emittime.We've done this through somecompiler optimizations that makesure that work is done in frontinstead of runtime.We've also deferred a lot of ourwork so that they're done on theInstruments backend.And that means that whilesignposts are being emitted,they should take very few systemresources.We've done this because we wantto minimize the impact towhatever your code is running.And we've also done it becausewe want to make sure that evenif you have very small timespan, you can emit a lot ofsignposts to get somefine-grained measurements.
But you may want to be able toturn your signposts off.Maybe you want to eliminate asmuch overhead as you can from aparticular code path.Or you might have two categoriesof signposts, both of which aresuper-high volume and you reallyare only interested in debuggingone or the other at a givenpoint in time.
Well, to do that we're going totake advantage of a feature ofOSLog, the disabled log handle.
So the disabled log handle is asimple handle.And what it does is every OSLogand os signpost call madeagainst that handle will justturn into something very closeto a no-op.
In fact, if you adopt this in C,we'll even do the check for youin line and then we won't evenevaluate the rest of thearguments.
So you can just change thishandle at runtime.Let me show you an example.
So we're going to go back to thevery first example code that wehad.And you see that initializationof that log handle up top.
Well, instead I'm going to makethat initialization conditional.So I'm either going to assign itto the normal os log constructoror I'm going to assign it to that disabled log handle.If we take the first path, allthe os signpost calls will workas I described, but if we takethe second path, those ossignpost calls will turn intonear no-ops.
So as I said before, notice thatI didn't have to call any of mycall -- I didn't have to changeany of my call sites.I only had to change the initialization.
And I made the initializationconditional on an environmentvariable. This is the kind of thing thatyou can set up in your Xcodescheme while you're debuggingyour program.
Now I said you didn't have tochange in the call sites, butmaybe you have somefunctionality that isinstrumentation specific.That is, it might be expensivebut it might only be used forwhile debugging.
So in that case, you can check aparticular log handle to see ifsignposts are turned on for itwith the signposts enabledproperty.The signposts enabled propertycan then be used to gate thatadditional operation.
Okay.So all the examples that I'veshown so far have been in Swift,but signposts are also availablein C.
All the functionality I've talked about so far isavailable: the long handles,emitting the three differentkinds of signposts, and managingyour signpost identifiers.
For those of you who are interested in adopting in C, Iencourage you to read the headerdoc. and header doc covers all thisinformation that I have but froma C developer's perspective.
All right.
Now you've seen how to adoptsignposts in your code.And maybe you have a mentalmodel of what they represent.So I would love for you to seehow signposts work in concertwith Instruments.And for that, I'm going to turnit over for the rest of thesession to my colleague, Chad.
Thank you.
All right.
Thank you, Shane.
Now today I want to show you anddemonstrate for you three newimportant features inInstruments 10 to help you workwith signpost data.
The first is the new os signpostinstrument.And that instrument allows youto record, visualize, andanalyze all of the signpostactivity in your application.
The next feature is points ofinterest.I'll talk a little bit about what points of interest are andwhen you might want to emit one.And then I'm also going to showyou the new custom instrumentsfeature and how you can use itwith os signposts to get a more,I guess, refined presentation ofyour signposts.
So let's take a look at that ina demonstration.
Okay.
Now to start with, we're goingto take a look at our exampleapplication first.And that is our Trailblazerapplication.
This app is -- shows you thelocal hiking trails.And it basically downloads thesebeautiful images for you as wescroll.
Now you'll notice that initiallywe have a white background andthen the image comes in laterand fills in. And this is a pretty commonpattern in an application likethis. And sometimes it's implementedwith a future or a promise butthis pattern -- as much as ithelps with performance, it'salso pretty difficult toprofile.And the reason for that isbecause there are a lot of asynchronous activities goingon. As the user scrolls, there aredownloads that are in-flight atthe same time. And if the user scrolls reallyquickly like this then thedownload may not complete beforethe image cell needs to bereused.And so then we have to cancelthat download.If we fail to do that, then weend up with several downloadsrunning in parallel that wedidn't really want.
So let's take a look at how wecan use signposts to analyze theapplication of our Trailblazer.
Now inside the trail cell, wehave a method calledstartImageDownload.And this is invoked when we needto download that new image, andit's passed in the image namethat should be downloaded.
Now we have a download helperclass here that we create aninstance of an pass in the nameand set ourself as the delegateso it'll call us back when it'sdownloaded.And in this case, since thedownloader represents theconcurrent activity that's goingon, this asynchronous work, it'sa great basis for a signpost ID.So we're going to create oursignpost ID using our downloaderobject.
Now to start our signposts,we're going to do an os signpostbegin.And we're going to send it toour networking log handle sotake a real quick look at ournetworking log handle.You see we're using ourTrailblazer bundle ID and acategory of networking.
Now we're going to pass an imageor, sorry, a signpost name ofbackground image so that way wecan see all of our backgroundimage downloads.And it will pass that signpostID that we created.And we'll attach some metadatato begin to convey the name ofthe image that we aredownloading.
So then we'll start ourdownload, and we'll set ourproperty to track that.We have it currentlyrunningDownloader.
Now when that finishes, we'llget this didReceiveImagecallback here.And we'll set our image view tothe image that we received.
And we'll call end on thesignpost. And we'll use the exact same loghandle, the same name, the samesignpost ID but this time we'regoing to attach some endmetadata to say finished withsize.
And you'll notice here thatwe've annotated this particularparameter with Xcode colonsize-in-bytes.And what this does is it tellsXcode and Instruments that thisargument should be treated as asize-in-bytes for both displayand analysis.
Now these are called engineeringtypes.And they can be read about inthe Instruments developer helpguide which is under the helpmenu in Instruments.
Now once we've completed ourdownloading, we can set thatback to nil.Now there are two ways that wecan finish a download.That was the success path.And then we have to consider thecancel path.
So in prepare for reuse, if wecurrently have a runningdownloader, we're going to needto first cancel that downloader.
So in that case, we're going toemit an end for the interval,and we're going to use that samelogging handle, signpost name,signpost ID.And we're going to use cancelledas the end metadata to separateit from when we finishsuccessfully.
Now that's enough to actually dosome profiling.So we're going to go over hereto a product profile.And that will start upInstruments once we've finishedbuilding and installing.That will start up Instrumentshere.And we can create a new blankdocument.Then we can go to the library,and I can show you how to usethat new os signpost instrument.So we have our new os signpostinstrument here.And we'll just drag and dropthat out into the trace.We'll make a little bit of roomhere for it and then we willpress record.And I'll bring our iPhone backup here to the beginning.All right.So now we'll do some scrollingand then we'll also do somereally, really quick scrolling.And then we'll let that settledown.Now we can go back toInstruments and see what kind ofdata we recorded.
So I'm going to stop therecording.
And now you'll notice here that,in the track view, we have avisualization of all of ourbackground image intervals.Now that's the signpost name.Now if we hold down the optionkey and we zoom in, you can seethere are intervals.And intervals are annotated withthe start metadata and the endmetadata.
Now if we zoom back out and thentake a look at the trace hereagain, we'll notice that we haveno more than five images thatare running downloads inparallel, which is a good thing.That means that our cancellationworked. And if we want to confirm that,we can come in here and you cansee that a lot of theseintervals have metadata thatsays that it was cancelled inthe download.
Now if you want to take -- ifyou want to look at a numerical-- or let's say you want to lookat the durations of theseintervals then you can come downhere to the summary ofintervals.And we see a breakdown bycategory and then by signpostname and then by the startmessage and then by the endmessage.
So if we make this a little bitsmaller, you can see that wemade 93 image download requests.
Of those, 12 were for locationone.
Of those 12, seven werecancelled and five finished witha size of 3.31 megabytes.
Now if you look over here, theseare the statistics about ourdurations.And you can see that the minimumand the average of the cancelledintervals is significantlysmaller than when we finishedwith the full downloads.And that's exactly what youwould expect to see in thispattern.
Now if you want to see all ofthe cancelled events becauseyou're interested in seeingthose, you can put this -- focusarrow and it will take you to alist view where you can see allthe places where location onehad an end message of cancelled.
And as we go through this,you'll see that the inspectionhead on the top of the tracewill move forward to each one ofthose intervals.So you can track all the failurecases if that's what you'reinterested in. Now that's a great way to viewthe times of those intervals,the timing of those intervals.But what if you wanted to do ananalysis of the metadata?What if you wanted to determinehow many bytes of image datathat we've downloaded over thenetwork?Well, we've emitted metadatamessages like finished with sizeand then the size.It would be great if we couldtotal that argument up.
So if you want to do that, youcan come over here to thesummary of metadata statistics.
You can see that we have itbroken down by the subsystem,the category, and then theformat string and then below theformat string, the argumentswithin that format string.And since our format string onlyhas one, that's simply arg0.Now Instruments has totaled thisup. And it knows that this is asize-in-bytes.And so it gives us a nicecalculation of 80 megabytes.So we've downloaded 80 megabytesof image data total.Now you can see the differentcolumns here.You've got it min, max, average,and standard deviation.So this is a great way to take alook at just statisticalanalysis of the values thatyou're conveying through yourmetadata.
Now Shane mentioned thatsignposts were very lightweightand that is totally true exceptwhen you run Instruments the wayI just ran Instruments.In what we call immediate mode,which is the default recordingmode, Instruments is trying toshow and record the data in nearreal time.
And so when it goes into immediate mode recording, allthe signposts have to be sentdirectly to Instruments.And we have to bypass all ofthose optimizations that you getfrom buffering in the operatingsystem.
Now with our signposts -- withour signposts application here,we're not really emitting enoughintervals to really notice thatoverhead, but if you have a gameengine and you want to emitthousands of signposts persecond, that overhead will startto build up.So in order to work around that,what you can do is change therecording mode of Instrumentsbefore you take your recording.And the way you do that is byholding down on the recordbutton and selecting recordingoptions.
And then in this section herefor the global options, you cansee that we have our immediatemode selected.And we can change that to lastfive second mode.Now this is often calledwindowed mode.
And what it tells the operatingsystem and the recordingtechnology is that we don't needevery single event.We just want the last fiveseconds worth.And when you do that,Instruments will step out of theway and let the operating systemdo what it does.Now this is a very common mode.We use this in system trace.We use this in metal systemtrace and the new gameperformance template.And so it's a very common way tolook for stutters and hangs inyour application. All right.
So that is our os signpostinstrument.
Now let's talk about points ofinterest.
Now if we come back to ourTrailblazer application here,you notice that when I tap on atrail, it pushes a detail.
If I go back and tap on adifferent trail, it'll push adifferent detail.
Now it would be great if wecould track every time thesedetail views come forwardbecause then we can tell whatour user is trying to do, and wecan tell where our user is inthe application.
Now you could certainly do thiswith a signpost, but you'd haveto drag in the os signpostinstrument and record all of thesignpost activity on theapplication.And it sort of dilutes howimportant these user navigationevents are.
So what we allow you to do ispromote them to what are calledpoints of interest.
Now if I go to our code for thedetail controller and we look atour viewDidAppear method, youcan see that I'm posting -- I'mcreating an os signpost eventsaying that a detail appearedand with the name of the detail.
Now this is sent to a speciallog handle that we've createdcalled points of interest.And the way that you create thatis by creating a log handle withyour subsystem identifier andthe systems points of interestcategory.So this is a special categorythat Instruments will be lookingfor.And when it sees points here,it'll place them into the pointsof interest instrument.So if we come back here and takea time profile, you can see thatwe have our points of interestinstrument automaticallyincluded.And if we do a recording and wedo that same basic action, we gointo the Matt Davis Trail andthen we'll come back here toSkyline Trail and then we'll goback and we'll do one more forgood measure.
Now when you go back toInstruments, you can see that wehave those points of interestprominently displayed.So you can see where your userwas in the navigation of yourapp. And you can correlate this withother performance data.
And so points of interest arereally a way for you to pick andchoose some of the mostimportant points of interest inyour application and make themavailable to every developerthat's on your team or in yourdevelopment community.And they can be seen right herein the points of interest.
All right.So that is the points ofinterest instrument and how youcreate points of interest fromsignposts.
Now another great feature ofInstruments 10 is the abilityfor you to create custominstruments.And so to demonstrate what youcan do with custom instrumentsin os signpost, we've created,as part of our project, aTrailblazer instruments package.
Now I'm going to build and runthat now.And you'll see when we do that,we start a separate copy ofInstruments that has our justbuilt package installed.And if we bring that versionforward, we'll see that we nowhave a Trailblazer networkingtrace template.And if we choose that, we cansee that we have a Trailblazernetworking instrument in ourtrace document.
And let's take a recording andsee what the difference isbetween our points of interestor, I'm sorry, our os signpostand what this custom instrumentcan do.So we'll do the same type ofthing. We'll do some basic downloading.And then we'll come back andwe'll analyze our trace.
Now the presentation here issignificantly different.So let's zoom in and take a lookat it.You'll notice here on the left,instead of breaking it down bysignpost name, we've broken itdown by the image beingdownloaded.So now we can see that image twowas downloaded at this point andat this point.
Now we've labeled each one withthe size in megabytes of thedownload.
And we've also colored them redif the download size is largerthan 3 1/2 megabytes.
So this is a custom graph thatwe created as part of our custominstrument. Now we've also defined somedetails down here.We've got a very simple list ofour download events.And, again, you can navigatethrough the trace with those.
And we also have -- let me seeif I can get this back intofocus here. We also have a summary for allthe downloads.Very simple.We just want to do a count and asum.And then we also have this coolthing called Timeslice.
And in a Timeslice view, whatwe're trying to do is answerthat question I was askingbefore of how many of thesethings are actually running inparallel?Well, if you want to take a lookat the intervals running inparallel, you just scrub theinspection head over herethrough time, and you can seeexactly what is intersectingthat inspection head at anygiven point in time.So it's a great and a differentway to look at your signpostdata.
Now if you are working withothers on a project or you'repart of the development community, using a custominstrument is a great way totake the signpost data andreshape it in a way that someoneelse can use and interpretwithout having to know thedetails of how your code worksso they are a very importantfeature.Now the great news is that tocreate an instrument like that,the entire package definition isabout 115 lines of XML and socustom instruments is veryexpressive and very powerful butalso very easy.
And that is the conclusion ofour demo.
So in today's session, we took alook at the signpost API, and weshowed you how to use it toannotate interesting periods ofactivity and intervals insideyour application.
We showed you how to collectmetadata and get that metadatainto Instruments forvisualization and analysis.And we showed you how to combinecustom instruments in ossignpost to create a moretailored presentation of yoursignpost data.
Now all this really comes downto us being able to give you thekind of information that youneed to help you tune theperformance of your application.And so we're really excited tosee how you use os signpost andInstruments together to improvethe user experience of yourapplication.
That is the content for today.For more information, you cancome see us in a lab, technologylab 8 at 3:00 today.And I also have session 410,creating custom instrumentstomorrow where I'll be goingover the details of how custominstruments works and show youhow we created our Trailblazernetworking instruments package.
Thank you very much.Enjoy the rest of your conference.[ Applause ]
[8]ページ先頭