- Performance Overview
- Performance Frequently Asked Questions (FAQ)
- Performance best practices
- Using the Portals ARM Token
- Extension load shim dependencies (removing shims)
- Performance profiling
- V2 targets
- Dependency injected view models
- Fast extension load
Portal performance from a customer's perspective is seen as all experiences throughout the product. As an extension author you have a duty to uphold your experience to the performance bar at a minimum.
Area | 95th Percentile Bar | Telemetry Action | How is it measured? |
---|---|---|---|
Extension | < 2 secs | ExtensionLoad | The time it takes for your extension's home page to be loaded and initial scripts, the initialize call to complete within your Extension definition file |
Blade | < 4 secs | BladeFullReady | The time it takes for the blade's onInitialize or onInputsSet to resolve and all the parts on the blade to become ready |
Part | < 4 secs | PartReady | Time it takes for the part to be rendered and then the part's OnInputsSet to resolve |
WxP | > 80 | N/A | An overall experience score, calculated by weighting blade usage and the blade full ready time |
Extension-loading performance effects both Blade and Part performance, as your extension is loaded and unloaded as and when it is required. In the case where a user is visiting your resource blade for the first time, the Fx will load up your extension and then request the view model, consequently your Blade/Part performance is affected. If the user were to browse away from your experience and browse back before your extension is unloaded, obviously the user's second visit will be faster, as they don't pay the cost of loading the extension.
Blade performance is spread across a couple of main areas. The best way to see how your scenario maps to these buckets is to take a browser performance profile.
BladeFullReady can be broken down into 4 stages:
- If the extension isn't loaded, load the extension
- Download and parse the required dependencies for the blade
- Execute and wait for the blade's
onInitialize()
promise to resolve - Process promise resolution from the main thread and complete the initial rendering of the Blade.
All of these perf costs are represented under the one BladeFullReady
action and the full end to end duration is tracked under the duration
column.
For an additional breakdown of the time spent you can inspect a native performance profile or the data
column of the BladeFullReady
telemetry event to find the following properties:
Stage | Native marker identifier | Data property name | Description |
---|---|---|---|
0 | ExtLoadBladeBundles | bundleLoadingTime | The async time spent requiring the BladeDefinition (which today is co-bundled with the Blade class’ module). This covers the time downloading and processing your Blade’s bundles. |
1 | ExtInstantiateBladeClass | Not Tracked | The async time spent diContainer.getAsync’ing the Blade class. This and the following ‘ExtBladeOnInitializeSync’ show up as insignificantly small, which itself can help refocus on larger time-slices. |
2 | ExtBladeOnInitializeSync | Not Tracked | The sync time spent in the Blade’s ‘onInitialize’ method. |
3 | ExtBladeOnInitializeAsync | onInitializeAsyncTime | The async time from the point ‘onInitialized’ is called to the point where the Promise returned from ‘onInitialize’ is resolved. All these are measured in the extension web worker. |
* | ExtBladePrepareFirstAjax | prepareFirstAjaxTime | The time spent from the point ‘onInitialized’ is called to the point where the first ajax call is sent from the extension web worker. This is fuzzy because the FX ajax client isn’t explicitly bound to a Blade, but inaccuracies should be outlier cases and should be easy to exclude based on knowledge of the Blade. |
If your blade is a FrameBlade or AppBlade there is an additional initialization message from your iframe to your viewmodel which is also tracked, see the samples extension framepage.js for an example of what messages are required.
BladeFullRender is the time it takes your experience to become FullReady + finish updating the UI. This means you are no longer populating the UI or changing observables bound to the UI.
BladeFullRender
duration is logged as an additional property, bladeRenderTime
, in the BladeFullReady
telemetry action's data column.
Similar to Blade performance, Part performance is spread across a couple of areas:
- Part's constructor
- Part's 'onInitialize' or 'onInputsSet'
If your part is a FramePart there is an additional initialization message from your iframe to your viewmodel which is also tracked, see the samples extension framepage.js for an example of what messages are required.
All of these perf costs are represented under the one PartReady
action.
There are two methods to assess your performance:
-
Visit the IbizaFx provided PowerBi report*
-
Run Kusto queries locally to determine your numbers
(*) To get access to the PowerBi dashboard reference the Telemetry onboarding guide, then access the following Extension performance/reliability report
The first method is definitely the easiest way to determine your current assessment as this is maintained on a regular basis by the Fx team. You can, if preferred, run queries locally but ensure you are using the Fx provided Kusto functions to calculate your assessment.
database('Framework').ExtensionPerformanceKPI(ago(1h), now())
ExtensionPerformance will return a table with the following columns:
- Extension
- The name of the extension
- Loads
- How many times the extension was loaded within the given date range
- 50th, 80th, 95th
- The time it takes for your extension to initialize. This is captured under the
ExtensionLoad
action in telemetry
- The time it takes for your extension to initialize. This is captured under the
- HostingServiceloads
- The number of loads from the hosting service
- UsingTheHostingService
- If the extension is predominantly using the hosting service in production
database('Framework').BladePerformanceIncludingNetwork(ago(1h), now())
With the BladePerformanceIncludingNetwork
function, we sample 1% of traffic to measure the number of network requests that are made throughout their session, that sampling does not affect the overall duration that is reported. Within the function we will correlate the count of any network requests, these are tracked in telemetry under the action XHRPerformance
, made when the user is loading a given blade. It does not impact the markers that measure performance. That said a larger number of network requests will generally result in slower performance.
The subtle difference with the standard BladeFullReady
marker is that if the blade is opened within a resource menu blade we will attribute the time it takes to resolve the getMenuConfig
promise as the resource menu blade is loaded to the 95th percentile of the 'BladeFullReady' duration. This is attributed using a proportional calculation based on the number of times the blade is loaded inside the menu blade.
For example, a blade takes 2000ms to complete its BladeFullReady
and 2000ms to return its getMenuConfig
.
It is only loaded once (1) in the menu blade out of its 10 loads. Its overall reported FullDuration would be 2200ms.
BladePerformanceIncludingNetwork will return a table with the following columns
- FullBladeName, Extension, BladeName
- Blade/Extension identifiers
- BladeCount
- The number of blade loads within the given date range
- InMenuLoads
- The number of in menu blade loads within the given date range
- PctOfMenuLoads
- The percentage of in menu blade loads within the given date range
- Samples
- The number of loads which were tracking the number of XHR requests
- StaticMenu
- If the
getMenuConfig
call returns within < 10ms, only applicable to ResourceMenu cases
- If the
- MenuConfigDuration95
- The 95th percentile of the
getMenuConfig
call
- The 95th percentile of the
- LockedBlade
- If the blade is locked, ideally blades are now template blades or no-pdl
- All no-pdl and template blades are locked, pdl blades can be made locked by setting the locked property to true
- XHRCount, XHRCount95th, XHRMax
- The 50th percentile (95th or MAX) of XHR requests sent which correlate to that blade load
- Bytes
- Bytes transferred to the client via XHR requests
- FullDuration50, 80, 95, 99
- The time it takes for the
BladeFullReady
+ (PctOfMenuLoads
* thegetMenuConfig
to resolve)
- The time it takes for the
- RedScore
- Number of violations for tracked bars
- AlertSeverity
- If the blade has opted to be alerted against via the alerting infrastructure and what severity the alert will open at.
database('Framework').PartPerformance(ago(1h), now())
PartPerformance will return a table with the following columns:
- FullPartName, Extension, PartName
- Part/Extension identifiers
- PartCount
- How many times the part was loaded within the given date range
- 50th, 80th, 95th, 99th
- The time it takes for your part to resolve its
onInputsSet
oronInitialize
promise. This is captured under thePartReady
action in telemetry
- The time it takes for your part to resolve its
- RedScore Number of violations for tracked bars
- Profile what is happening in your extension load. Profile your scenario
- Are you using the Portal's ARM token? If no, verify if you can use the Portal's ARM token and if yes, follow: Using the Portal's ARM token
- Are you on the hosting service? If no, migrate to the hosting service: Hosting service documentation
- If you are, have you enabled prewarming?
- Follow http://aka.ms/portalfx/docs/prewarming to enable prewarming for your extension load.
- If you are, have you enabled prewarming?
- Are you using obsolete bundles?
- If yes, remove your dependency to them and then remove the obsolete bitmask. This is a blocking download before your extension load. See below for further details.
- See our best practices
- Assess what is happening in your Blades's
onInitialize
(no-PDL) or constructor andonInputsSet
(PDL). Profile your scenario- Can that be optimized?
- If there are any AJAX calls;
- Can they use batch? If so, migrate over to use the batch api.
- Wrap them with custom telemetry and ensure they you aren't spending a large amount of time waiting on the result. If you are to do this, please only log one event per blade load, this will help correlate issues but also reduce unneccesary load on telemetry servers.
- Are you using an old PDL "Blade containing Parts"? How many parts are on the blade?
- If there is only a single part, if you're not using a no-pdl blade or
<TemplateBlade>
migrate your current blade to a no-pdl blade. - If there are multiple parts, migrate over to use a no-pdl blade.
- Ensure to support any old pinned parts when you migrate.
- If there is only a single part, if you're not using a no-pdl blade or
- Does your blade open within a resource menu blade?
- If it does, ensure the
getMenuConfig
call is returned statically/synchronously (< 10ms). You can make use of the enabled/disabled observable property on menu items, if you need to asynchronously determine to enable a menu item.
- If it does, ensure the
- See our best practices
- Assess what is happening in your Part's
onInitialize
(no-PDL) or constructor andonInputsSet
(PDL), including time taken in any async operations associated with the returned Promise. Profile your scenario- Can that be optimized?
- If there are any AJAX calls;
- Can they use batch? If so, migrate over to use the batch api.
- Wrap them with custom telemetry and ensure they you aren't spending a large amount of time waiting on the result. If you are to do this, please only log one event per part load, this will help correlate issues but also reduce unneccesary load on telemetry servers.
- See our best practices
Using the Extension performance/reliability report you can see the WxP impact for each individual blade. Although given the Wxp calculation, if you are drastically under the bar its likely a high usage blade is not meeting the performance bar, if you are just under the bar then it's likely it's a low usage blade which is not meeting the bar.
Sure! Book in some time in the Azure performance office hours.
- When? Wednesdays from 13:00 to 16:00
- Where? B41 (Conf Room 41/44)
- Contacts: Sean Watson (sewatson)
- Goals
- Help extensions to meet the performance bar
- Help extensions to measure performance
- Help extensions to understand their current performance status
- How to book time: Send a meeting request with the following
- TO: sewatson;
- Subject: YOUR_EXTENSION_NAME: Azure performance office hours
- Location: Conf Room 41/44 (It is already reserved)
- Migrate to the hosting service
- Enable prewarming, running your extension in a web worker
- Ensure your extension isn't using shims
- Migrate your extension to dependency injection
- Migrate your extension to Fast extension load
- Ensure your extension isn't using obsolete bundles
- Use the Portal's ARM delegation token
- Enable performance alerts
- To ensure your experience never regresses unintentionally, you can opt into configurable alerting on your extension, blade and part load times. See performance alerting
- Move to hosting service
- We've seen every team who have onboarded to the hosting service get some performance benefit from the migration.
- Performance benefits vary from team to team given your current infrastructure
- We've commonly seen teams improve their performance by > 0.5s at the 95th
- If you are not on the hosting service ensure;
- Homepage caching is enabled
- Persistent content caching is enabled
- Compression is enabled
- Your service is efficiently geo-distributed (Note: we have seen better performance from having an actual presence in a region vs a CDN)
- We've seen every team who have onboarded to the hosting service get some performance benefit from the migration.
- Compression (Brotli)
- Move to V2 targets to get this by default, see V2 targets
- Remove controllers
- Don't proxy ARM through your controllers
- Don't require full libraries to make use of a small portion
- Is there another way to get the same result?
- If you're using iframe experiences
- Ensure you have the correct caching enabled
- Ensure you have compression enabled
- Your bundling logic is optimised
- Are you serving your iframe experience geo-distributed efficiently?
- Reduce network calls
- Ideally 1 network call per blade
- Utilise
batch
to make network requests, see our batch documentation
- Remove automatic polling
- If you need to poll, only poll on the second request and ensure
isBackgroundTask: true
in the batch call
- If you need to poll, only poll on the second request and ensure
- Remove all dependencies on obsoleted code
- Loading any required obsoleted bundles is a blocking request during your extension load times
- See https://aka.ms/portalfx/obsoletebundles for further details
- Use the Portal's ARM token
- Don't use old PDL blades composed of parts: hello world template blade
- Each part on the blade has its own viewmodel and template overhead, switching to a no-pdl template blade will mitigate that overhead
- Use the latest controls available: see https://aka.ms/portalfx/playground
- This will minimise your observable usage
- The newer controls are AMD'd reducing what is required to load your blade
- Remove Bad CSS selectors
- Build with warnings as errors and fix them
- Bad CSS selectors are defined as selectors which end in HTML elements for example
.class1 .class2 .class3 div { background: red; }
- Since CSS is evaluated from right-to-left the browser will find all
div
elements first, this is obviously expensive
- Since CSS is evaluated from right-to-left the browser will find all
- Fix your telemetry
- Ensure you are returning the relevant blocking promises as part of your initialization path (
onInitialize
oronInputsSet
), today you maybe cheating the system but that is only hurting your users. - Ensure your telemetry is capturing the correct timings
- Ensure you are returning the relevant blocking promises as part of your initialization path (
- Test your scenarios at scale
- How does your scenario deal with 100s of subscriptions or 1000s of resources?
- Are you fanning out to gather all subscriptions, if so do not do that.
- Limit the default experience to first N subscriptions and have the user determine their own scope.
- Develop in diagnostics mode
- Use https://portal.azure.com?trace=diagnostics to detect console performance warnings
- Using too many defers
- Using too many ko.computed dependencies
- Use https://portal.azure.com?trace=diagnostics to detect console performance warnings
- Be wary of observable usage
- Try not to use them unless necessary
- Don't aggressively update UI-bound observables
- Accumulate the changes and then update the observable
- Manually throttle or use
.extend({ rateLimit: 250 });
when initializing the observable
This request is a blocking call before your extension can start loading. This drastically hurts performance and even more so at the higher percentiles.
If you're migrating to use the Portals ARM Token please verify if you are relying on server side validation of the token first.
Below is an example PR of another team making this change. Example PR
Ensure you verify:
- If you do not require your own token, and you currently aren’t relying on server side validation of the token you should be able to make the change easily.
- If you do require your own token, assess if that is necessary and migrate to the Portal’s token if possible.
- If you’re relying on server side validation, please update that validation to validate the Portal App Id instead – if that is sufficient for you.
To fix this it is a simple change to the Portal’s config here: extensions.prod.json See below for further details.
Please send a pull request to the portal’s config with your change. Unfortunately, we don’t like to make config changes on behalf of extensions.
- To send a pull request first create a work item
- Then create a new branch from that work item via the ‘create a new branch’ link
- Make your required changes in the correct files
- Send the PR and include GuruA and SanSom as the reviewers.
Please make this change in all applicable environments, dogfood, PROD, FF, BF, and MC.
The config files follow the naming convention of Extension.*.json
– where * is the environment.
You need to move the oAuthClientId and oAuthClientCertificate properties to be defined on the non-arm resourceAccess. See the PR below for an example of these changes. Example PR
Extension load shim dependencies are dependencies that are hardcoded into your require config to be downloaded and executed before any other script can be executed. Because of the way shims work in requireJS, no assumption can be made about their usage and you end up blocking download and execution of any dependent script until shims are all fully downloaded and executed. For most extensions today, at best (latest SDK), what this translates to is a shim bundle being downloaded concurrently with your extension's entrypoint bundle, meaning a blocking download of OSS libraries that delays any work to initialize your extension.
To fix this, you have a few options:
- Reevaluate the need for that library
- We've seen giant OSS libraries being pulled for very little functionality that the Portal actually provided or that could have been extracted/reimplemented with way less code downloaded, so confirming you really need said library is the first thing you should look into.
- Convert the library to an AMD module
- By converting the library to an AMD module and adding an amd-dependency tag to files that really need it, you enable the Portal's bundler to bundle said library with its owners (saving a network round-trip) and you move it out of the extension init path.
Converting your OSS library to an AMD module is very straightforward in most cases. What you want to do is wrap all of the library’s code with a define statement like this:
define([], function() {
/** OSS code here **/
});
Taking an actual file as an example, here's the diff for the hammer.js OSS library:
--------------------------------------------------------------------------
4 4 * Copyright (c) 2014 Jorik Tangelder <[email protected]>;
5 5 * Licensed under the MIT license */
7 + define([], function () {
7 8 (function (window, undefined) {
8 9 'use strict';
10 11 /**
--------------------------------------------------------------------------
2156 2157 window.Hammer = Hammer;
2158 2159 })(window);
2159 2160 + });
--------------------------------------------------------------------------
The next step is to add an amd-dependency tag to all the files that use the aforementioned OSS library so that the TypeScript to JavaScript transpilation generates an explicit dependency for it.
Simply insert:
/// <amd-dependency path="dependencyPath" />
At the top of your file, where dependencyPath is the path to your AMD OSS library. For hammer.js:
/// <amd-dependency path="hammer" />
Finally, since the bundler will now automatically pick up the library’s file and bundle it properly, you can remove the shim code from your C#; you can find said code by looking for derived classes of ContentBundleDefinition.
This should cover the vast majority of shim-to-AMD conversion cases. For more information, please create a stack overflow question (https://aka.ms/portalfx/ask) and reach out to [email protected].
- Open a browser and load portal using
https://portal.azure.com/?clientoptimizations=bundle&feature.nativeperf=true
clientOptimizations=bundle
will allow you to assess which bundles are being downloaded in a user friendly mannerfeature.nativeperf=true
will expose native performance markers within your profile traces, allowing you to accurately match portal telemetry markers to the profile
- Go to a blank dashboard
- Clear cache (hard reset), remove all application data and reload the portal
- Use the browsers profiling timeline to throttle both network and CPU, this best reflects the 95th percentile scenario, then start the profiler
- Walk through your desired scenario
- Switch to the desired dashboard
- Deep link to your blade, make sure to keep the feature flags in the deep link. Deeplinking will isolate as much noise as possible from your profile
- Stop the profiler
- Assess the profile
- Blocking network calls
- Fetching data - We've seen often that backend services don't have the same performance requirements as the front end experience, because of which you may need to engage your backend team/service to ensure your front end experience can meet the desired performance bar.
- Required files - Downloading more than what is required, try to minimise your total payload size.
- Heavy rendering and CPU from overuse of UI-bound observables
- Are you updating the same observable repeatedly in a short time frame? Is that reflected in the DOM in any way? Do you have computeds listening to a large number of observables?
To correctly verify a change you will need to ensure the before and after are instrumented correctly with telemetry. Without that you cannot truly verify the change was helpful. We have often seen what seems like a huge win locally transition into a smaller win once it's in production, we've also seen the opposite occur too. The main take away is to trust your telemetry and not profiling, production data is the truth.
The Azure Portal SDK ships a "V2" targets that is designed to work with CloudBuild. The Azure Portal team and some of the larger extension partners teams have already enabled CloudBuild for their repositories to using the V2 targets. The key value proposition of the V2 targets are:
- Support for compile-on-save of TypeScript files that works with both Visual Studio and VSCode.
- A highly reliable incremental compilation engine that can significantly reduce local development build times.
- Support automatically serving content files that are compressed using max Brotli compression. This feature will help extension performance at the 95th percentile where network latency and throughput dominates.
Below are the steps to switch to the V2 targets. A video of the migration steps can be found here: https://msit.microsoftstream.com/video/49879891-7735-44c0-9255-d32162b78ed5?st=1349
- Get your extension working with at least Ibiza SDK 5.0.302.6501. The V2 targets are under active development are continuously being improved. Ideally get your extension working with the latest SDK.
-
Fully build your extension to get all of the code-generated files (eg. TypeScript files generated from PDL) generated.
-
Delete any generate d.ts files generated in
$(ProjectDir)Client\Definitions
. You do not have to do anything to files outside of the Client folder. -
Add a tsconfig.json to the root of the project with the following content. '''Do not deviate unless you know what you are doing.
{
"compileOnSave": true,
"compilerOptions": {
"baseUrl": "Client",
"declaration": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": true,
"module": "amd",
"moduleResolution": "node",
"noEmitHelpers": true,
"noImplicitAny": true,
"noImplicitThis": true,
"paths": {
"*": [
"*"
]
},
"outDir": "Output/Content/Scripts",
"rootDir": "Client",
"removeComments": false,
"sourceMap": true,
"target": "es5",
"types": []
},
"include": [
"Client/**/*"
]
}
- If the framework d.ts files (e.g. MsPortalFx.d.ts) for your extension are in
$(ProjectDir)\Definitions
, the tsconfig.json "include" setting will not include these files. To include these files for compilation, create the file$(ProjectDir)Client\TypeReferences.d.ts
and add reference tags to these files. You can also include these files by specifying them in the include section of the tsconfig.json. - Run tsc.exe TypeScript compiler that is shipped with the Portal SDK with the project folder as current directory. This will compile the TypeScript files using the tsconfig.json file.
- You may see new errors because the TypeScript compiler is more strict in checking code when using a tsconfig file. Fix any errors that you see. You may need to remove
/// <reference path="" />
lines from all TypeScript files to fix certain errors. - If you see a casing mismatch error, you may need to use "git mv" to rename and change the casing of the file.
- Remove all
<TypeScriptCompile>
elements from the csproj. Do not remove the<SvgTs>
tags. If you use Visual Studio and want to see TypeScript files in the Solution Explorer, you should instead change the element names to None or Content. - Remove all TypeScript and PDL MSBuild properties from the csproj. These include:
<PropertyGroup>
<TypeScriptExperimentalDecorators>true</TypeScriptExperimentalDecorators>
<PortalDefinitionTargetFolder>Client</PortalDefinitionTargetFolder>
<PortalDefinitionContentName>.</PortalDefinitionContentName>
<PortalDefinitionWriteAmd>true</PortalDefinitionWriteAmd>
<EmbeddedTypeScriptResourcePrefixReplace>Client\</EmbeddedTypeScriptResourcePrefixReplace>
<EmbeddedTypeScriptResourcePrefix>Content\Scripts\</EmbeddedTypeScriptResourcePrefix>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" Label="TypeScriptConfigurationsDebug">
<TypeScriptNoImplicitAny>true</TypeScriptNoImplicitAny>
<TypeScriptTarget>ES5</TypeScriptTarget>
<TypeScriptRemoveComments>false</TypeScriptRemoveComments>
<TypeScriptSourceMap>true</TypeScriptSourceMap>
<TypeScriptGeneratesDeclarations>false</TypeScriptGeneratesDeclarations>
<TypeScriptModuleKind>AMD</TypeScriptModuleKind>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TypeScriptNoImplicitAny>true</TypeScriptNoImplicitAny>
<TypeScriptTarget>ES5</TypeScriptTarget>
<TypeScriptRemoveComments>true</TypeScriptRemoveComments>
<TypeScriptSourceMap>true</TypeScriptSourceMap>
<TypeScriptGeneratesDeclarations>false</TypeScriptGeneratesDeclarations>
<TypeScriptModuleKind>AMD</TypeScriptModuleKind>
</PropertyGroup>
- Add a content tag for the tsconfig.json file to the .csproj:
<Content Include="tsconfig.json" />
- Switch the old tools target to the new tools target ("v2") in the .csproj. The new import targets looks something like:
<Import Project="$(PkgMicrosoft_Portal_Tools)\build\Microsoft.Portal.Tools.targets" />
- Add the following to the csproj inside an ItemGroup if you have any
<Svg>
tags in the csproj. This tag informs CloudBuild that Svg MsBuild Items are consider inputs to the project.
<AvailableItemName Include="Svg">
<Visible>False</Visible>
</AvailableItemName>
- Make sure that the
Microsoft.Portal.Tools.targets
is imported after the C# and WebApplication targets. The ordering should look like something before.
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0\WebApplications\Microsoft.WebApplication.targets" />
<Import Project="$(NuGetPath_Microsoft_Portal_Tools)\build\Microsoft.Portal.Tools.targets" Condition="Exists('$(NuGetPath_Microsoft_Portal_Tools)\build\Microsoft.Portal.Tools.targets')" />
- The output location of pde files has been changed from
$(ProjectDir)Client
to$(OutDir)
.
The framework supports loading view models using dependency injection. If you migrate your extension to use this programming model, the SDK will no longer generate ViewModelFactories.ts and a large portion of ExtensionDefinition.ts. Consequently you can remove nearly all code in Program.ts. All of your DataContext classes will also be bundled with the associated blade and will no longer be loaded up front.
If you have any issues throughout this process please post to our stack overflow
- Migrate to V2 targets if you haven’t done so (See: V2 targets)
- Ensure that the
emitDecoratorMetadata
compiler option is set totrue
in the tsconfig.json - Ensure that the
forceConsistentCasingInFileNames
compiler option is set totrue
in the tsconfig.json - Ensure that the
moduleResolution
compiler option is set tonode
in the tsconfig.json - Upgrade to at least SDK 3001+
- Cleanup your extension project TypeScript code and remove all uses of export = Main.
- Check this PR in the portal repo for an example: https://msazure.visualstudio.com/One/_git/AzureUX-PortalFx/pullrequest/1003495?_a=overview
- You do not have to remove trailing newlines like the PR.
- Commit and verify that these changes do not break your extension before starting the actual migration.
- Remove the code in Program.ts that initializes the DataContext classes. Set the generic type parameter of
MsPortalFx.Extension.EntryPointBase
base class specification to void. - Delete the generated ViewModelFactories.ts from
Client\_generated
- Add the following line to your csproj
<EnableDependencyInjectedViewModels>true</EnableDependencyInjectedViewModels>
- Build the extension project
- Get a copy of the dependency injection migration tool at: \\wimoy-dev\Public\DependencyInjectionMigrator and copy it locally. Many thanks to Bryan Wood ([email protected]) for improving the tool.
- Look for the string "ViewModels:" in the build logs and copy and paste the JSON to Extension.json in the dependency injection migration tool.
- Modify the migration tool source code and put in the path of the folder that contains the TypeScript for your extension
- Run the tool and migrate your V1 view models.
- The tool will modify your source files and perform the following operations:
- Add
import * as Di from "Fx/DependencyInjection
to the top of any file with a V1 (pdl) view model - Add
@Di.Class("viewModel")
right before every single V1 view model class - Delete the initialState second parameter of the viewModel classes
- Add
- The migration tool is based on regex and is not perfect. Review the results and make any necessary adjustments to ensure that it performs those three operations on all V1 viewModels.
- The removal of the initialState parameter may cause breaks in your code if your code was referencing the parameter. The portal was always passing null for initialState. You can basically remove all uses of initialState.
- If the tool outputs anything besides a completion message, send wimoy an email with the message
- The tool will modify your source files and perform the following operations:
- Optionally, remove any parameters in V1 view models that are no longer needed. In the process of doing so, you may end up with some unused DataContext classes too. You can remove them if they are not used by V2 (no-pdl) view models.
- Find all V2 view models and add the InjectableModel decorator. Refer to the PRs below for examples.
- You can enumerate all of the V2 view models by going through the code in the following generated folders located at the root of your TypeScript build:
- _generated\adapters\blade
- _generated\adapters\part
- DataContext classes referenced by V2 view models cannot be removed even if they are empty
- You can enumerate all of the V2 view models by going through the code in the following generated folders located at the root of your TypeScript build:
- Find all DataContext classes that are still referenced by your view models and add the
@Di.Class()
decorator.- Note that
@Di.Class()
is called with no arguments. - You will need to add
import * as Di from "Fx/DependencyInjection
to the top of the files
- Note that
- The constructor of any class that contains a
@Di.Class()
decorator (with or without the "viewModel" argument) cannot contain an parameter that is specified with a non-class type. Some of your view model classes may have a dataContext parameter with an any type or an interface type. Either change the type to a class or remove the parameter entirely. - All classes in the dependency chain of migrated view models should be marked with
@Di.Class()
decorator. The dependency injection framework in the Portal only supports constructor injection. - Put the following code in your Program.ts right at the module level. Then load your extension through the portal. This will validate that you have correctly migrated the V1 view models. The code should complete almost instantly. Remove the code when you are done.
MsPortalFx.require("Fx/DependencyInjection")
.then((di: any) => {
const container: any = di.createContainer("viewModel");
(function (array: any[]) {
array.forEach(a => {
if (a.module) {
MsPortalFx.require(a.module)
.then((m: any) => {
console.log("Loading view model: " + a.module + " " + a.export);
const exportedType = m[a.export];
if (exportedType.ViewModelAdapter) {
// Can't validate V2 view models
}
else {
container._validate(new (<any>window).Map(), exportedType, true);
}
});
}
});
})([/* insert view model json from build log here */ ]);
});
- Temporarily set
emitDecoratorMetadata
compiler option to false. Then turn on the compiler optionnoUnusedParameters
andnoUnusedLocals
. Remove any dead parameters flagged by the compiler. You may find some violations in generated code. Ignore them.
- Note: as of sdk 5.0.302.20501 Program.ts should be removed completely as part of this migration.
- https://msazure.visualstudio.com/One/_git/AzureUX-PortalFx/pullrequest/1013125?_a=overview
- https://msazure.visualstudio.com/One/_git/AzureUX-PortalFx/pullrequest/1013301?_a=overview
- https://msazure.visualstudio.com/One/_git/AzureUX-PortalFx/pullrequest/1016472?_a=overview
- https://msazure.visualstudio.com/One/_git/AD-IAM-IPC/pullrequest/1096247?_a=overview
- https://msazure.visualstudio.com/One/_git/AD-IAM-Services-ADIbizaUX/pullrequest/1098977?_a=overview
- https://msazure.visualstudio.com/One/_git/MGMT-AppInsights-InsightsPortal/pullrequest/1124038?_a=overview
The frameworks supports a new extension load contract that can improve extension load performance by one second at the 95th percentile by deprecating Program.ts and the classic extension initialization code path. Once your extension uses the new contract, the portal will no longer download and execute Program.ts and _generated/Manifest.ts. _generated/ExtensionDefinition.ts will be bundled with your blades.
- Remove all requireJS shims.
- Complete the dependency injected view models migration.
- Upgrade to at least SDK 14401.
- The SDK can be updated from the internal package feeds.
- $(ExtensionPageVersion) breaking change notes: https://msazure.visualstudio.com/One/_workitems/edit/3276047
- Prewarming / Web Workers is not a pre-requisite. If an extension onboards to both Prewarming and FastExtensionLoad, the framework will eliminate an additional 500 ms postMessage call, allowing an extension to reach sub-second extension load time.
-
Since the new extension load contract will no longer execute Program.ts, your extension's Program.ts should only contain the bare minimum scaffolding. Refer to the following Program.ts for an example: https://msazure.visualstudio.com/One/_git/AzureUX-PortalFx/pullrequest/1320194?_a=files&path=%2Fsrc%2FSDK%2FAcceptanceTests%2FExtensions%2FInternalSamplesExtension%2FExtension%2FClient%2FProgram.ts
-
You do not need to run
MsPortalFx.Base.Diagnostics.Telemetry.initialize(*extensionName*);
because the framework will run it on your behalf. -
If your extension is on the hosting service, you can delete Program.ts.
-
If you have RPC callbacks that need to be registered, you need to migrate them to the new contract by performing the following steps.
-
Create the file
Client\EventHandlers\EventHandlers.ts
. -
Create a class like the one below and add your RPC registrations.
import * as Di from "Fx/DependencyInjection"; import Rpc = MsPortalFx.Services.Rpc; @Di.Class() @Rpc.EventHandler.Decorator("rpc") export class EventHandlers { public registerEndPoints(): void { // Add RPC registrations here } }
- Refer to these changes for an example: https://msazure.visualstudio.com/One/_git/AzureUX-IaaSExp/commit/fba28b74f52b4d8a60497037f9ecd743ff775368?path=%2Fsrc%2Fsrc%2FUx%2FExtensions%2FCompute%2FClient%2FEventHandlers%2FEventHandlers.ts&gridItemType=2&_a=contents
- You can verify whether the RPC callbacks are registered correctly by checking
Output/Content/AzurePortalMetadata/SdkSuppliedEnvironment.json
forrpc
.
-
-
Change the
EnableDependencyInjectedViewModels
MSBuild property in your csproj toEnableFastExtensionLoad
. -
The URI used to register your extension to the portal should be the application root and should not contain any routes.
-
You may need to change the URI that you use to sideload your extension.
-
The hosting service URIs are already registered correctly.
-
You can add a urlMapping in your web.config to redirect the root application path
~/
to your home page controller. This change does not have to be deployed to production if your extension is already on the hosting service.<system.web> <urlMappings enabled="true"> <add url="~/" mappedUrl="~/Home/Index"/> </urlMappings> </system.web>
-
-
You can verify whether the migration was completed successfully by sideloading your extension in MPAC and checking whether the expression
FxImpl.Extension.isFastExtensionLoadEnabled()
returnstrue
in the iframe/webworker of your extension.
- https://msazure.visualstudio.com/One/_git/AzureUX-Monitoring/pullrequest/1514753
- https://dev.azure.com/msazure/One/_git/Mgmt-RecoverySvcs-Portal/pullrequest/1423720
- https://msazure.visualstudio.com/One/_git/MGMT-AppInsights-InsightsPortal/pullrequest/1426564
- https://msazure.visualstudio.com/One/_git/AzureUX-Monitoring/pullrequest/1514753