Explore K-Means clustering using R and Tidy data principles.
In this lesson, you will learn how to create clusters using the
Tidymodels package and other packages in the R ecosystem (we’ll call
them friends 🧑🤝🧑), and the Nigerian music dataset you imported earlier.
We will cover the basics of K-Means for Clustering. Keep in mind that,
as you learned in the earlier lesson, there are many ways to work with
clusters and the method you use depends on your data. We will try
K-Means as it’s the most common clustering technique. Let’s get
started!
Terms you will learn about:
Silhouette scoring
Elbow method
Inertia
Variance
Introduction
K-Means
Clustering is a method derived from the domain of signal processing.
It is used to divide and partition groups of data into
k clusters
based on similarities in their features.
The clusters can be visualized as Voronoi diagrams,
which include a point (or ‘seed’) and its corresponding region.
Infographic by Jen Looper
K-Means clustering has the following steps:
The data scientist starts by specifying the desired number of
clusters to be created.
Next, the algorithm randomly selects K observations from the data
set to serve as the initial centers for the clusters (i.e.,
centroids).
Next, each of the remaining observations is assigned to its
closest centroid.
Next, the new means of each cluster is computed and the centroid
is moved to the mean.
Now that the centers have been recalculated, every observation is
checked again to see if it might be closer to a different cluster. All
the objects are reassigned again using the updated cluster means. The
cluster assignment and centroid update steps are iteratively repeated
until the cluster assignments stop changing (i.e., when convergence is
achieved). Typically, the algorithm terminates when each new iteration
results in negligible movement of centroids and the clusters become
static.
Note that due to randomization of the initial k observations used as
the starting centroids, we can get slightly different results each time
we apply the procedure. For this reason, most algorithms use several
random starts and choose the iteration with the lowest WCSS. As
such, it is strongly recommended to always run K-Means with several
values of nstart to avoid an undesirable local
optimum.
This short animation using the artwork
of Allison Horst explains the clustering process:
Artwork by @allison_horst
A fundamental question that arises in clustering is this: how do you
know how many clusters to separate your data into? One drawback of using
K-Means includes the fact that you will need to establish
k
, that is the number of centroids
.
Fortunately the elbow method
helps to estimate a good
starting value for k
. You’ll try it in a minute.
Prerequisite
We’ll pick off right from where we stopped in the previous
lesson, where we analysed the data set, made lots of visualizations
and filtered the data set to observations of interest. Be sure to check
it out!
We’ll require some packages to knock-off this module. You can have
them installed as:
install.packages(c('tidyverse', 'tidymodels', 'cluster', 'summarytools', 'plotly', 'paletteer', 'factoextra', 'patchwork'))
Alternatively, the script below checks whether you have the packages
required to complete this module and installs them for you in case some
are missing.
suppressWarnings(if(!require("pacman")) install.packages("pacman",repos = "http://cran.us.r-project.org"))
## Loading required package: pacman
pacman::p_load('tidyverse', 'tidymodels', 'cluster', 'summarytools', 'plotly', 'paletteer', 'factoextra', 'patchwork')
##
## The downloaded binary packages are in
## /var/folders/c9/r3f6t3kj3wv9jrh50g63hp1r0000gn/T//RtmpHKd9vp/downloaded_packages
##
## summarytools installed
## Warning in pacman::p_load("tidyverse", "tidymodels", "cluster", "summarytools", : Failed to install/load:
## summarytools
Let’s hit the ground running!
1. A dance with data: Narrow down to the 3 most popular music
genres
This is a recap of what we did in the previous lesson. Let’s slice
and dice some data!
# Load the core tidyverse and make it available in your current R session
library(tidyverse)
# Import the data into a tibble
df <- read_csv(file = "https://raw.githubusercontent.com/microsoft/ML-For-Beginners/main/5-Clustering/data/nigerian-songs.csv", show_col_types = FALSE)
# Narrow down to top 3 popular genres
nigerian_songs <- df %>%
# Concentrate on top 3 genres
filter(artist_top_genre %in% c("afro dancehall", "afropop","nigerian pop")) %>%
# Remove unclassified observations
filter(popularity != 0)
# Visualize popular genres using bar plots
theme_set(theme_light())
nigerian_songs %>%
count(artist_top_genre) %>%
ggplot(mapping = aes(x = artist_top_genre, y = n,
fill = artist_top_genre)) +
geom_col(alpha = 0.8) +
paletteer::scale_fill_paletteer_d("ggsci::category10_d3") +
ggtitle("Top genres") +
theme(plot.title = element_text(hjust = 0.5))
🤩 That went well!
2. More data exploration.
How clean is this data? Let’s check for outliers using box plots. We
will concentrate on numeric columns with fewer outliers (although you
could clean out the outliers). Boxplots can show the range of the data
and will help choose which columns to use. Note, Boxplots do not show
variance, an important element of good clusterable data. Please see this
discussion for further reading.
Boxplots are
used to graphically depict the distribution of numeric
data, so let’s start by selecting all numeric columns alongside
the popular music genres.
# Select top genre column and all other numeric columns
df_numeric <- nigerian_songs %>%
select(artist_top_genre, where(is.numeric))
# Display the data
df_numeric %>%
slice_head(n = 5)
See how the selection helper where
makes this easy 💁?
Explore such other functions here.
Since we’ll be making a boxplot for each numeric features and we want
to avoid using loops, let’s reformat our data into a longer
format that will allow us to take advantage of facets
-
subplots that each display one subset of the data.
# Pivot data from wide to long
df_numeric_long <- df_numeric %>%
pivot_longer(!artist_top_genre, names_to = "feature_names", values_to = "values")
# Print out data
df_numeric_long %>%
slice_head(n = 15)
Much longer! Now time for some ggplots
! So what
geom
will we use?
# Make a box plot
df_numeric_long %>%
ggplot(mapping = aes(x = feature_names, y = values, fill = feature_names)) +
geom_boxplot() +
facet_wrap(~ feature_names, ncol = 4, scales = "free") +
theme(legend.position = "none")
Easy-gg!
Now we can see this data is a little noisy: by observing each column
as a boxplot, you can see outliers. You could go through the dataset and
remove these outliers, but that would make the data pretty minimal.
For now, let’s choose which columns we will use for our clustering
exercise. Let’s pick the numeric columns with similar ranges. We could
encode the artist_top_genre
as numeric but we’ll drop it
for now.
# Select variables with similar ranges
df_numeric_select <- df_numeric %>%
select(popularity, danceability, acousticness, loudness, energy)
# Normalize data
# df_numeric_select <- scale(df_numeric_select)
3. Computing k-means clustering in R
We can compute k-means in R with the built-in kmeans
function, see help("kmeans()")
. kmeans()
function accepts a data frame with all numeric columns as it’s primary
argument.
The first step when using k-means clustering is to specify the number
of clusters (k) that will be generated in the final solution. We know
there are 3 song genres that we carved out of the dataset, so let’s try
3:
set.seed(2056)
# Kmeans clustering for 3 clusters
kclust <- kmeans(
df_numeric_select,
# Specify the number of clusters
centers = 3,
# How many random initial configurations
nstart = 25
)
# Display clustering object
kclust
## K-means clustering with 3 clusters of sizes 65, 111, 110
##
## Cluster means:
## popularity danceability acousticness loudness energy
## 1 53.40000 0.7698615 0.2684248 -5.081200 0.7167231
## 2 31.28829 0.7310811 0.2558767 -5.159550 0.7589279
## 3 10.12727 0.7458727 0.2720171 -4.586418 0.7906091
##
## Clustering vector:
## [1] 2 3 2 2 2 2 2 2 2 3 2 2 3 2 1 2 3 3 1 3 1 1 1 3 1 2 1 1 2 2 3 3 1 2 2 2 2
## [38] 3 3 1 2 1 2 1 2 1 1 3 3 2 3 1 1 2 2 2 2 3 3 1 3 2 2 3 2 2 3 2 3 2 2 3 3 3
## [75] 3 3 2 3 2 2 1 2 3 3 3 2 2 2 2 3 2 2 2 2 3 3 2 3 3 2 3 2 3 2 3 2 2 3 2 1 3
## [112] 3 2 3 3 2 2 2 2 2 2 2 1 3 3 3 3 1 3 2 3 2 3 2 2 2 1 2 3 3 3 2 3 1 3 2 2 3
## [149] 3 3 1 3 2 2 2 3 3 1 3 2 3 3 3 3 2 1 1 1 3 1 1 1 1 1 1 2 1 3 1 1 3 1 1 2 1
## [186] 1 3 3 2 1 2 2 1 2 2 3 3 1 3 3 1 1 3 1 2 1 3 1 2 1 1 2 2 2 3 3 3 3 3 1 2 2
## [223] 2 2 2 3 3 3 3 3 2 2 3 3 1 3 3 3 1 2 2 2 3 3 1 1 3 3 2 1 1 1 1 1 2 1 1 2 3
## [260] 3 3 2 2 2 3 2 3 2 3 3 3 1 2 2 2 3 2 3 1 3 2 3 3 3 2 3
##
## Within cluster sum of squares by cluster:
## [1] 3550.293 4559.358 4889.010
## (between_SS / total_SS = 85.8 %)
##
## Available components:
##
## [1] "cluster" "centers" "totss" "withinss" "tot.withinss"
## [6] "betweenss" "size" "iter" "ifault"
The kmeans object contains several bits of information which is well
explained in help("kmeans()")
. For now, let’s focus on a
few. We see that the data has been grouped into 3 clusters of sizes 65,
110, 111. The output also contains the cluster centers (means) for the 3
groups across the 5 variables.
The clustering vector is the cluster assignment for each observation.
Let’s use the augment
function to add the cluster
assignment the original data set.
# Add predicted cluster assignment to data set
augment(kclust, df_numeric_select) %>%
relocate(.cluster) %>%
slice_head(n = 10)
Perfect, we have just partitioned our data set into a set of 3
groups. So, how good is our clustering 🤷? Let’s take a look at the
Silhouette score
Silhouette score
Silhouette
analysis can be used to study the separation distance between the
resulting clusters. This score varies from -1 to 1, and if the score is
near 1, the cluster is dense and well-separated from other clusters. A
value near 0 represents overlapping clusters with samples very close to
the decision boundary of the neighboring clusters. (Source).
The average silhouette method computes the average silhouette of
observations for different values of k. A high average
silhouette score indicates a good clustering.
The silhouette
function in the cluster package to
compuate the average silhouette width.
The silhouette can be calculated with any distance metric, such as the Euclidean distance or the Manhattan distance which we discussed in
the previous
lesson.
# Load cluster package
library(cluster)
# Compute average silhouette score
ss <- silhouette(kclust$cluster,
# Compute euclidean distance
dist = dist(df_numeric_select))
mean(ss[, 3])
## [1] 0.5494668
Our score is .549, so right in the middle. This
indicates that our data is not particularly well-suited to this type of
clustering. Let’s see whether we can confirm this hunch visually. The factoextra
package provides functions (fviz_cluster()
) to
visualize clustering.
library(factoextra)
# Visualize clustering results
fviz_cluster(kclust, df_numeric_select)
The overlap in clusters indicates that our data is not particularly
well-suited to this type of clustering but let’s continue.
4. Determining optimal clusters
A fundamental question that often arises in K-Means clustering is
this - without known class labels, how do you know how many clusters to
separate your data into?
One way we can try to find out is to use a data sample to
create a series of clustering models
with an incrementing
number of clusters (e.g from 1-10), and evaluate clustering metrics such
as the Silhouette score.
Let’s determine the optimal number of clusters by computing the
clustering algorithm for different values of k and evaluating
the Within Cluster Sum of Squares (WCSS). The total
within-cluster sum of square (WCSS) measures the compactness of the
clustering and we want it to be as small as possible, with lower values
meaning that the data points are closer.
Let’s explore the effect of different choices of k
, from
1 to 10, on this clustering.
# Create a series of clustering models
kclusts <- tibble(k = 1:10) %>%
# Perform kmeans clustering for 1,2,3 ... ,10 clusters
mutate(model = map(k, ~ kmeans(df_numeric_select, centers = .x, nstart = 25)),
# Farm out clustering metrics eg WCSS
glanced = map(model, ~ glance(.x))) %>%
unnest(cols = glanced)
# View clustering rsulsts
kclusts
Now that we have the total within-cluster sum-of-squares
(tot.withinss) for each clustering algorithm with center k, we
use the elbow
method to find the optimal number of clusters. The method consists
of plotting the WCSS as a function of the number of clusters, and
picking the elbow of the curve as the number of
clusters to use.
set.seed(2056)
# Use elbow method to determine optimum number of clusters
kclusts %>%
ggplot(mapping = aes(x = k, y = tot.withinss)) +
geom_line(size = 1.2, alpha = 0.8, color = "#FF7F0EFF") +
geom_point(size = 2, color = "#FF7F0EFF")
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
The plot shows a large reduction in WCSS (so greater
tightness) as the number of clusters increases from one to two,
and a further noticeable reduction from two to three clusters. After
that, the reduction is less pronounced, resulting in an
elbow
💪in the chart at around three clusters. This is a
good indication that there are two to three reasonably well separated
clusters of data points.
We can now go ahead and extract the clustering model where
k = 3
:
pull()
: used to extract a single column
pluck()
: used to index data structures such as lists
# Extract k = 3 clustering
final_kmeans <- kclusts %>%
filter(k == 3) %>%
pull(model) %>%
pluck(1)
final_kmeans
## K-means clustering with 3 clusters of sizes 111, 110, 65
##
## Cluster means:
## popularity danceability acousticness loudness energy
## 1 31.28829 0.7310811 0.2558767 -5.159550 0.7589279
## 2 10.12727 0.7458727 0.2720171 -4.586418 0.7906091
## 3 53.40000 0.7698615 0.2684248 -5.081200 0.7167231
##
## Clustering vector:
## [1] 1 2 1 1 1 1 1 1 1 2 1 1 2 1 3 1 2 2 3 2 3 3 3 2 3 1 3 3 1 1 2 2 3 1 1 1 1
## [38] 2 2 3 1 3 1 3 1 3 3 2 2 1 2 3 3 1 1 1 1 2 2 3 2 1 1 2 1 1 2 1 2 1 1 2 2 2
## [75] 2 2 1 2 1 1 3 1 2 2 2 1 1 1 1 2 1 1 1 1 2 2 1 2 2 1 2 1 2 1 2 1 1 2 1 3 2
## [112] 2 1 2 2 1 1 1 1 1 1 1 3 2 2 2 2 3 2 1 2 1 2 1 1 1 3 1 2 2 2 1 2 3 2 1 1 2
## [149] 2 2 3 2 1 1 1 2 2 3 2 1 2 2 2 2 1 3 3 3 2 3 3 3 3 3 3 1 3 2 3 3 2 3 3 1 3
## [186] 3 2 2 1 3 1 1 3 1 1 2 2 3 2 2 3 3 2 3 1 3 2 3 1 3 3 1 1 1 2 2 2 2 2 3 1 1
## [223] 1 1 1 2 2 2 2 2 1 1 2 2 3 2 2 2 3 1 1 1 2 2 3 3 2 2 1 3 3 3 3 3 1 3 3 1 2
## [260] 2 2 1 1 1 2 1 2 1 2 2 2 3 1 1 1 2 1 2 3 2 1 2 2 2 1 2
##
## Within cluster sum of squares by cluster:
## [1] 4559.358 4889.010 3550.293
## (between_SS / total_SS = 85.8 %)
##
## Available components:
##
## [1] "cluster" "centers" "totss" "withinss" "tot.withinss"
## [6] "betweenss" "size" "iter" "ifault"
Great! Let’s go ahead and visualize the clusters obtained. Care for
some interactivity using plotly
?
# Add predicted cluster assignment to data set
results <- augment(final_kmeans, df_numeric_select) %>%
bind_cols(df_numeric %>% select(artist_top_genre))
# Plot cluster assignments
clust_plt <- results %>%
ggplot(mapping = aes(x = popularity, y = danceability, color = .cluster, shape = artist_top_genre)) +
geom_point(size = 2, alpha = 0.8) +
paletteer::scale_color_paletteer_d("ggthemes::Tableau_10")
ggplotly(clust_plt)
Perhaps we would have expected that each cluster (represented by
different colors) would have distinct genres (represented by different
shapes).
Let’s take a look at the model’s accuracy.
# Assign genres to predefined integers
label_count <- results %>%
group_by(artist_top_genre) %>%
mutate(id = cur_group_id()) %>%
ungroup() %>%
summarise(correct_labels = sum(.cluster == id))
# Print results
cat("Result:", label_count$correct_labels, "out of", nrow(results), "samples were correctly labeled.")
## Result: 109 out of 286 samples were correctly labeled.
cat("\nAccuracy score:", label_count$correct_labels/nrow(results))
##
## Accuracy score: 0.3811189
This model’s accuracy is not bad, but not great. It may be that the
data may not lend itself well to K-Means Clustering. This data is too
imbalanced, too little correlated and there is too much variance between
the column values to cluster well. In fact, the clusters that form are
probably heavily influenced or skewed by the three genre categories we
defined above.
Nevertheless, that was quite a learning process!
In Scikit-learn’s documentation, you can see that a model like this
one, with clusters not very well demarcated, has a ‘variance’
problem:
Infographic from Scikit-learn
Variance
Variance is defined as “the average of the squared differences from
the Mean” (Source).
In the context of this clustering problem, it refers to data that the
numbers of our dataset tend to diverge a bit too much from the mean.
✅ This is a great moment to think about all the ways you could
correct this issue. Tweak the data a bit more? Use different columns?
Use a different algorithm? Hint: Try scaling
your data to normalize it and test other columns.
Try this ‘variance
calculator’ to understand the concept a bit more.
🚀Challenge
Spend some time with this notebook, tweaking parameters. Can you
improve the accuracy of the model by cleaning the data more (removing
outliers, for example)? You can use weights to give more weight to given
data samples. What else can you do to create better clusters?
Hint: Try to scale your data. There’s commented code in the notebook
that adds standard scaling to make the data columns resemble each other
more closely in terms of range. You’ll find that while the silhouette
score goes down, the ‘kink’ in the elbow graph smooths out. This is
because leaving the data unscaled allows data with less variance to
carry more weight. Read a bit more on this problem here.
Review & Self Study
Take a look at a K-Means Simulator such
as this one. You can use this tool to visualize sample data points
and determine its centroids. You can edit the data’s randomness, numbers
of clusters and numbers of centroids. Does this help you get an idea of
how the data can be grouped?
Also, take a look at this
handout on K-Means from Stanford.
Want to try out your newly acquired clustering skills to data sets
that lend well to K-Means clustering? Please see:
THANK YOU TO:
Jen Looper for
creating the original Python version of this module ♥️
Allison Horst
for creating the amazing illustrations that make R more welcoming and
engaging. Find more illustrations at her gallery.
Happy Learning,
Eric, Gold Microsoft Learn
Student Ambassador.
Artwork by @allison_horst
#{r include=FALSE} #library(here) #library(rmd2jupyter) #rmd2jupyter("lesson_14.Rmd") #
LS0tCnRpdGxlOiAnSy1NZWFucyBDbHVzdGVyaW5nIHVzaW5nIFRpZHltb2RlbHMgYW5kIGZyaWVuZHMnCm91dHB1dDoKICBodG1sX2RvY3VtZW50OgogICAgI2Nzczogc3R5bGVfNy5jc3MKICAgIGRmX3ByaW50OiBwYWdlZAogICAgdGhlbWU6IGZsYXRseQogICAgaGlnaGxpZ2h0OiBicmVlemVkYXJrCiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB5ZXMKICAgIGNvZGVfZG93bmxvYWQ6IHllcwotLS0KCiMjIEV4cGxvcmUgSy1NZWFucyBjbHVzdGVyaW5nIHVzaW5nIFIgYW5kIFRpZHkgZGF0YSBwcmluY2lwbGVzLgoKIyMjIFsqKlByZS1sZWN0dXJlIHF1aXoqKl0oaHR0cHM6Ly9ncmF5LXNhbmQtMDdhMTBmNDAzLjEuYXp1cmVzdGF0aWNhcHBzLm5ldC9xdWl6LzI5LykKCkluIHRoaXMgbGVzc29uLCB5b3Ugd2lsbCBsZWFybiBob3cgdG8gY3JlYXRlIGNsdXN0ZXJzIHVzaW5nIHRoZSBUaWR5bW9kZWxzIHBhY2thZ2UgYW5kIG90aGVyIHBhY2thZ2VzIGluIHRoZSBSIGVjb3N5c3RlbSAod2UnbGwgY2FsbCB0aGVtIGZyaWVuZHMg8J+nkeKAjfCfpJ3igI3wn6eRKSwgYW5kIHRoZSBOaWdlcmlhbiBtdXNpYyBkYXRhc2V0IHlvdSBpbXBvcnRlZCBlYXJsaWVyLiBXZSB3aWxsIGNvdmVyIHRoZSBiYXNpY3Mgb2YgSy1NZWFucyBmb3IgQ2x1c3RlcmluZy4gS2VlcCBpbiBtaW5kIHRoYXQsIGFzIHlvdSBsZWFybmVkIGluIHRoZSBlYXJsaWVyIGxlc3NvbiwgdGhlcmUgYXJlIG1hbnkgd2F5cyB0byB3b3JrIHdpdGggY2x1c3RlcnMgYW5kIHRoZSBtZXRob2QgeW91IHVzZSBkZXBlbmRzIG9uIHlvdXIgZGF0YS4gV2Ugd2lsbCB0cnkgSy1NZWFucyBhcyBpdCdzIHRoZSBtb3N0IGNvbW1vbiBjbHVzdGVyaW5nIHRlY2huaXF1ZS4gTGV0J3MgZ2V0IHN0YXJ0ZWQhCgpUZXJtcyB5b3Ugd2lsbCBsZWFybiBhYm91dDoKCi0gICBTaWxob3VldHRlIHNjb3JpbmcKCi0gICBFbGJvdyBtZXRob2QKCi0gICBJbmVydGlhCgotICAgVmFyaWFuY2UKCiMjIyAqKkludHJvZHVjdGlvbioqCgpbSy1NZWFucyBDbHVzdGVyaW5nXShodHRwczovL3dpa2lwZWRpYS5vcmcvd2lraS9LLW1lYW5zX2NsdXN0ZXJpbmcpIGlzIGEgbWV0aG9kIGRlcml2ZWQgZnJvbSB0aGUgZG9tYWluIG9mIHNpZ25hbCBwcm9jZXNzaW5nLiBJdCBpcyB1c2VkIHRvIGRpdmlkZSBhbmQgcGFydGl0aW9uIGdyb3VwcyBvZiBkYXRhIGludG8gYGsgY2x1c3RlcnNgIGJhc2VkIG9uIHNpbWlsYXJpdGllcyBpbiB0aGVpciBmZWF0dXJlcy4KClRoZSBjbHVzdGVycyBjYW4gYmUgdmlzdWFsaXplZCBhcyBbVm9yb25vaSBkaWFncmFtc10oaHR0cHM6Ly93aWtpcGVkaWEub3JnL3dpa2kvVm9yb25vaV9kaWFncmFtKSwgd2hpY2ggaW5jbHVkZSBhIHBvaW50IChvciAnc2VlZCcpIGFuZCBpdHMgY29ycmVzcG9uZGluZyByZWdpb24uCgohW0luZm9ncmFwaGljIGJ5IEplbiBMb29wZXJdKC4uLy4uL2ltYWdlcy92b3Jvbm9pLnBuZykKCkstTWVhbnMgY2x1c3RlcmluZyBoYXMgdGhlIGZvbGxvd2luZyBzdGVwczoKCjEuICBUaGUgZGF0YSBzY2llbnRpc3Qgc3RhcnRzIGJ5IHNwZWNpZnlpbmcgdGhlIGRlc2lyZWQgbnVtYmVyIG9mIGNsdXN0ZXJzIHRvIGJlIGNyZWF0ZWQuCgoyLiAgTmV4dCwgdGhlIGFsZ29yaXRobSByYW5kb21seSBzZWxlY3RzIEsgb2JzZXJ2YXRpb25zIGZyb20gdGhlIGRhdGEgc2V0IHRvIHNlcnZlIGFzIHRoZSBpbml0aWFsIGNlbnRlcnMgZm9yIHRoZSBjbHVzdGVycyAoaS5lLiwgY2VudHJvaWRzKS4KCjMuICBOZXh0LCBlYWNoIG9mIHRoZSByZW1haW5pbmcgb2JzZXJ2YXRpb25zIGlzIGFzc2lnbmVkIHRvIGl0cyBjbG9zZXN0IGNlbnRyb2lkLgoKNC4gIE5leHQsIHRoZSBuZXcgbWVhbnMgb2YgZWFjaCBjbHVzdGVyIGlzIGNvbXB1dGVkIGFuZCB0aGUgY2VudHJvaWQgaXMgbW92ZWQgdG8gdGhlIG1lYW4uCgo1LiAgTm93IHRoYXQgdGhlIGNlbnRlcnMgaGF2ZSBiZWVuIHJlY2FsY3VsYXRlZCwgZXZlcnkgb2JzZXJ2YXRpb24gaXMgY2hlY2tlZCBhZ2FpbiB0byBzZWUgaWYgaXQgbWlnaHQgYmUgY2xvc2VyIHRvIGEgZGlmZmVyZW50IGNsdXN0ZXIuIEFsbCB0aGUgb2JqZWN0cyBhcmUgcmVhc3NpZ25lZCBhZ2FpbiB1c2luZyB0aGUgdXBkYXRlZCBjbHVzdGVyIG1lYW5zLiBUaGUgY2x1c3RlciBhc3NpZ25tZW50IGFuZCBjZW50cm9pZCB1cGRhdGUgc3RlcHMgYXJlIGl0ZXJhdGl2ZWx5IHJlcGVhdGVkIHVudGlsIHRoZSBjbHVzdGVyIGFzc2lnbm1lbnRzIHN0b3AgY2hhbmdpbmcgKGkuZS4sIHdoZW4gY29udmVyZ2VuY2UgaXMgYWNoaWV2ZWQpLiBUeXBpY2FsbHksIHRoZSBhbGdvcml0aG0gdGVybWluYXRlcyB3aGVuIGVhY2ggbmV3IGl0ZXJhdGlvbiByZXN1bHRzIGluIG5lZ2xpZ2libGUgbW92ZW1lbnQgb2YgY2VudHJvaWRzIGFuZCB0aGUgY2x1c3RlcnMgYmVjb21lIHN0YXRpYy4KCjxkaXY+Cgo+IE5vdGUgdGhhdCBkdWUgdG8gcmFuZG9taXphdGlvbiBvZiB0aGUgaW5pdGlhbCBrIG9ic2VydmF0aW9ucyB1c2VkIGFzIHRoZSBzdGFydGluZyBjZW50cm9pZHMsIHdlIGNhbiBnZXQgc2xpZ2h0bHkgZGlmZmVyZW50IHJlc3VsdHMgZWFjaCB0aW1lIHdlIGFwcGx5IHRoZSBwcm9jZWR1cmUuIEZvciB0aGlzIHJlYXNvbiwgbW9zdCBhbGdvcml0aG1zIHVzZSBzZXZlcmFsICpyYW5kb20gc3RhcnRzKiBhbmQgY2hvb3NlIHRoZSBpdGVyYXRpb24gd2l0aCB0aGUgbG93ZXN0IFdDU1MuIEFzIHN1Y2gsIGl0IGlzIHN0cm9uZ2x5IHJlY29tbWVuZGVkIHRvIGFsd2F5cyBydW4gSy1NZWFucyB3aXRoIHNldmVyYWwgdmFsdWVzIG9mICpuc3RhcnQqIHRvIGF2b2lkIGFuICp1bmRlc2lyYWJsZSBsb2NhbCBvcHRpbXVtLioKCjwvZGl2PgoKVGhpcyBzaG9ydCBhbmltYXRpb24gdXNpbmcgdGhlIFthcnR3b3JrXShodHRwczovL2dpdGh1Yi5jb20vYWxsaXNvbmhvcnN0L3N0YXRzLWlsbHVzdHJhdGlvbnMpIG9mIEFsbGlzb24gSG9yc3QgZXhwbGFpbnMgdGhlIGNsdXN0ZXJpbmcgcHJvY2VzczoKCiFbQXJ0d29yayBieSBcQGFsbGlzb25faG9yc3RdKC4uLy4uL2ltYWdlcy9rbWVhbnMuZ2lmKQoKQSBmdW5kYW1lbnRhbCBxdWVzdGlvbiB0aGF0IGFyaXNlcyBpbiBjbHVzdGVyaW5nIGlzIHRoaXM6IGhvdyBkbyB5b3Uga25vdyBob3cgbWFueSBjbHVzdGVycyB0byBzZXBhcmF0ZSB5b3VyIGRhdGEgaW50bz8gT25lIGRyYXdiYWNrIG9mIHVzaW5nIEstTWVhbnMgaW5jbHVkZXMgdGhlIGZhY3QgdGhhdCB5b3Ugd2lsbCBuZWVkIHRvIGVzdGFibGlzaCBga2AsIHRoYXQgaXMgdGhlIG51bWJlciBvZiBgY2VudHJvaWRzYC4gRm9ydHVuYXRlbHkgdGhlIGBlbGJvdyBtZXRob2RgIGhlbHBzIHRvIGVzdGltYXRlIGEgZ29vZCBzdGFydGluZyB2YWx1ZSBmb3IgYGtgLiBZb3UnbGwgdHJ5IGl0IGluIGEgbWludXRlLgoKIyMjIAoKKipQcmVyZXF1aXNpdGUqKgoKV2UnbGwgcGljayBvZmYgcmlnaHQgZnJvbSB3aGVyZSB3ZSBzdG9wcGVkIGluIHRoZSBbcHJldmlvdXMgbGVzc29uXShodHRwczovL2dpdGh1Yi5jb20vbWljcm9zb2Z0L01MLUZvci1CZWdpbm5lcnMvYmxvYi9tYWluLzUtQ2x1c3RlcmluZy8xLVZpc3VhbGl6ZS9zb2x1dGlvbi9SL2xlc3Nvbl8xNC1SLmlweW5iKSwgd2hlcmUgd2UgYW5hbHlzZWQgdGhlIGRhdGEgc2V0LCBtYWRlIGxvdHMgb2YgdmlzdWFsaXphdGlvbnMgYW5kIGZpbHRlcmVkIHRoZSBkYXRhIHNldCB0byBvYnNlcnZhdGlvbnMgb2YgaW50ZXJlc3QuIEJlIHN1cmUgdG8gY2hlY2sgaXQgb3V0IQoKV2UnbGwgcmVxdWlyZSBzb21lIHBhY2thZ2VzIHRvIGtub2NrLW9mZiB0aGlzIG1vZHVsZS4gWW91IGNhbiBoYXZlIHRoZW0gaW5zdGFsbGVkIGFzOiBgaW5zdGFsbC5wYWNrYWdlcyhjKCd0aWR5dmVyc2UnLCAndGlkeW1vZGVscycsICdjbHVzdGVyJywgJ3N1bW1hcnl0b29scycsICdwbG90bHknLCAncGFsZXR0ZWVyJywgJ2ZhY3RvZXh0cmEnLCAncGF0Y2h3b3JrJykpYAoKQWx0ZXJuYXRpdmVseSwgdGhlIHNjcmlwdCBiZWxvdyBjaGVja3Mgd2hldGhlciB5b3UgaGF2ZSB0aGUgcGFja2FnZXMgcmVxdWlyZWQgdG8gY29tcGxldGUgdGhpcyBtb2R1bGUgYW5kIGluc3RhbGxzIHRoZW0gZm9yIHlvdSBpbiBjYXNlIHNvbWUgYXJlIG1pc3NpbmcuCgpgYGB7cn0Kc3VwcHJlc3NXYXJuaW5ncyhpZighcmVxdWlyZSgicGFjbWFuIikpIGluc3RhbGwucGFja2FnZXMoInBhY21hbiIscmVwb3MgPSAiaHR0cDovL2NyYW4udXMuci1wcm9qZWN0Lm9yZyIpKQoKcGFjbWFuOjpwX2xvYWQoJ3RpZHl2ZXJzZScsICd0aWR5bW9kZWxzJywgJ2NsdXN0ZXInLCAnc3VtbWFyeXRvb2xzJywgJ3Bsb3RseScsICdwYWxldHRlZXInLCAnZmFjdG9leHRyYScsICdwYXRjaHdvcmsnKQpgYGAKCkxldCdzIGhpdCB0aGUgZ3JvdW5kIHJ1bm5pbmchCgojIyAxLiBBIGRhbmNlIHdpdGggZGF0YTogTmFycm93IGRvd24gdG8gdGhlIDMgbW9zdCBwb3B1bGFyIG11c2ljIGdlbnJlcwoKVGhpcyBpcyBhIHJlY2FwIG9mIHdoYXQgd2UgZGlkIGluIHRoZSBwcmV2aW91cyBsZXNzb24uIExldCdzIHNsaWNlIGFuZCBkaWNlIHNvbWUgZGF0YSEKCmBgYHtyIG1lc3NhZ2U9Riwgd2FybmluZz1GfQojIExvYWQgdGhlIGNvcmUgdGlkeXZlcnNlIGFuZCBtYWtlIGl0IGF2YWlsYWJsZSBpbiB5b3VyIGN1cnJlbnQgUiBzZXNzaW9uCmxpYnJhcnkodGlkeXZlcnNlKQoKIyBJbXBvcnQgdGhlIGRhdGEgaW50byBhIHRpYmJsZQpkZiA8LSByZWFkX2NzdihmaWxlID0gImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9taWNyb3NvZnQvTUwtRm9yLUJlZ2lubmVycy9tYWluLzUtQ2x1c3RlcmluZy9kYXRhL25pZ2VyaWFuLXNvbmdzLmNzdiIsIHNob3dfY29sX3R5cGVzID0gRkFMU0UpCgojIE5hcnJvdyBkb3duIHRvIHRvcCAzIHBvcHVsYXIgZ2VucmVzCm5pZ2VyaWFuX3NvbmdzIDwtIGRmICU+JSAKICAjIENvbmNlbnRyYXRlIG9uIHRvcCAzIGdlbnJlcwogIGZpbHRlcihhcnRpc3RfdG9wX2dlbnJlICVpbiUgYygiYWZybyBkYW5jZWhhbGwiLCAiYWZyb3BvcCIsIm5pZ2VyaWFuIHBvcCIpKSAlPiUgCiAgIyBSZW1vdmUgdW5jbGFzc2lmaWVkIG9ic2VydmF0aW9ucwogIGZpbHRlcihwb3B1bGFyaXR5ICE9IDApCgoKCiMgVmlzdWFsaXplIHBvcHVsYXIgZ2VucmVzIHVzaW5nIGJhciBwbG90cwp0aGVtZV9zZXQodGhlbWVfbGlnaHQoKSkKbmlnZXJpYW5fc29uZ3MgJT4lCiAgY291bnQoYXJ0aXN0X3RvcF9nZW5yZSkgJT4lCiAgZ2dwbG90KG1hcHBpbmcgPSBhZXMoeCA9IGFydGlzdF90b3BfZ2VucmUsIHkgPSBuLAogICAgICAgICAgICAgICAgICAgICAgIGZpbGwgPSBhcnRpc3RfdG9wX2dlbnJlKSkgKwogIGdlb21fY29sKGFscGhhID0gMC44KSArCiAgcGFsZXR0ZWVyOjpzY2FsZV9maWxsX3BhbGV0dGVlcl9kKCJnZ3NjaTo6Y2F0ZWdvcnkxMF9kMyIpICsKICBnZ3RpdGxlKCJUb3AgZ2VucmVzIikgKwogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpKQoKCmBgYAoK8J+kqSBUaGF0IHdlbnQgd2VsbCEKCiMjIDIuIE1vcmUgZGF0YSBleHBsb3JhdGlvbi4KCkhvdyBjbGVhbiBpcyB0aGlzIGRhdGE/IExldCdzIGNoZWNrIGZvciBvdXRsaWVycyB1c2luZyBib3ggcGxvdHMuIFdlIHdpbGwgY29uY2VudHJhdGUgb24gbnVtZXJpYyBjb2x1bW5zIHdpdGggZmV3ZXIgb3V0bGllcnMgKGFsdGhvdWdoIHlvdSBjb3VsZCBjbGVhbiBvdXQgdGhlIG91dGxpZXJzKS4gQm94cGxvdHMgY2FuIHNob3cgdGhlIHJhbmdlIG9mIHRoZSBkYXRhIGFuZCB3aWxsIGhlbHAgY2hvb3NlIHdoaWNoIGNvbHVtbnMgdG8gdXNlLiBOb3RlLCBCb3hwbG90cyBkbyBub3Qgc2hvdyB2YXJpYW5jZSwgYW4gaW1wb3J0YW50IGVsZW1lbnQgb2YgZ29vZCBjbHVzdGVyYWJsZSBkYXRhLiBQbGVhc2Ugc2VlIFt0aGlzIGRpc2N1c3Npb25dKGh0dHBzOi8vc3RhdHMuc3RhY2tleGNoYW5nZS5jb20vcXVlc3Rpb25zLzkxNTM2L2RlZHVjZS12YXJpYW5jZS1mcm9tLWJveHBsb3QpIGZvciBmdXJ0aGVyIHJlYWRpbmcuCgpbQm94cGxvdHNdKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0JveF9wbG90KSBhcmUgdXNlZCB0byBncmFwaGljYWxseSBkZXBpY3QgdGhlIGRpc3RyaWJ1dGlvbiBvZiBgbnVtZXJpY2AgZGF0YSwgc28gbGV0J3Mgc3RhcnQgYnkgKnNlbGVjdGluZyogYWxsIG51bWVyaWMgY29sdW1ucyBhbG9uZ3NpZGUgdGhlIHBvcHVsYXIgbXVzaWMgZ2VucmVzLgoKYGBge3Igc2VsZWN0fQojIFNlbGVjdCB0b3AgZ2VucmUgY29sdW1uIGFuZCBhbGwgb3RoZXIgbnVtZXJpYyBjb2x1bW5zCmRmX251bWVyaWMgPC0gbmlnZXJpYW5fc29uZ3MgJT4lIAogIHNlbGVjdChhcnRpc3RfdG9wX2dlbnJlLCB3aGVyZShpcy5udW1lcmljKSkgCgojIERpc3BsYXkgdGhlIGRhdGEKZGZfbnVtZXJpYyAlPiUgCiAgc2xpY2VfaGVhZChuID0gNSkKCmBgYAoKU2VlIGhvdyB0aGUgc2VsZWN0aW9uIGhlbHBlciBgd2hlcmVgIG1ha2VzIHRoaXMgZWFzeSDwn5KBPyBFeHBsb3JlIHN1Y2ggb3RoZXIgZnVuY3Rpb25zIFtoZXJlXShodHRwczovL3RpZHlzZWxlY3Quci1saWIub3JnLykuCgpTaW5jZSB3ZSdsbCBiZSBtYWtpbmcgYSBib3hwbG90IGZvciBlYWNoIG51bWVyaWMgZmVhdHVyZXMgYW5kIHdlIHdhbnQgdG8gYXZvaWQgdXNpbmcgbG9vcHMsIGxldCdzIHJlZm9ybWF0IG91ciBkYXRhIGludG8gYSAqbG9uZ2VyKiBmb3JtYXQgdGhhdCB3aWxsIGFsbG93IHVzIHRvIHRha2UgYWR2YW50YWdlIG9mIGBmYWNldHNgIC0gc3VicGxvdHMgdGhhdCBlYWNoIGRpc3BsYXkgb25lIHN1YnNldCBvZiB0aGUgZGF0YS4KCmBgYHtyIHBpdm90X2xvbmdlcn0KIyBQaXZvdCBkYXRhIGZyb20gd2lkZSB0byBsb25nCmRmX251bWVyaWNfbG9uZyA8LSBkZl9udW1lcmljICU+JSAKICBwaXZvdF9sb25nZXIoIWFydGlzdF90b3BfZ2VucmUsIG5hbWVzX3RvID0gImZlYXR1cmVfbmFtZXMiLCB2YWx1ZXNfdG8gPSAidmFsdWVzIikgCgojIFByaW50IG91dCBkYXRhCmRmX251bWVyaWNfbG9uZyAlPiUgCiAgc2xpY2VfaGVhZChuID0gMTUpCmBgYAoKTXVjaCBsb25nZXIhIE5vdyB0aW1lIGZvciBzb21lIGBnZ3Bsb3RzYCEgU28gd2hhdCBgZ2VvbWAgd2lsbCB3ZSB1c2U/CgpgYGB7cn0KIyBNYWtlIGEgYm94IHBsb3QKZGZfbnVtZXJpY19sb25nICU+JSAKICBnZ3Bsb3QobWFwcGluZyA9IGFlcyh4ID0gZmVhdHVyZV9uYW1lcywgeSA9IHZhbHVlcywgZmlsbCA9IGZlYXR1cmVfbmFtZXMpKSArCiAgZ2VvbV9ib3hwbG90KCkgKwogIGZhY2V0X3dyYXAofiBmZWF0dXJlX25hbWVzLCBuY29sID0gNCwgc2NhbGVzID0gImZyZWUiKSArCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQpgYGAKCkVhc3ktZ2chCgpOb3cgd2UgY2FuIHNlZSB0aGlzIGRhdGEgaXMgYSBsaXR0bGUgbm9pc3k6IGJ5IG9ic2VydmluZyBlYWNoIGNvbHVtbiBhcyBhIGJveHBsb3QsIHlvdSBjYW4gc2VlIG91dGxpZXJzLiBZb3UgY291bGQgZ28gdGhyb3VnaCB0aGUgZGF0YXNldCBhbmQgcmVtb3ZlIHRoZXNlIG91dGxpZXJzLCBidXQgdGhhdCB3b3VsZCBtYWtlIHRoZSBkYXRhIHByZXR0eSBtaW5pbWFsLgoKRm9yIG5vdywgbGV0J3MgY2hvb3NlIHdoaWNoIGNvbHVtbnMgd2Ugd2lsbCB1c2UgZm9yIG91ciBjbHVzdGVyaW5nIGV4ZXJjaXNlLiBMZXQncyBwaWNrIHRoZSBudW1lcmljIGNvbHVtbnMgd2l0aCBzaW1pbGFyIHJhbmdlcy4gV2UgY291bGQgZW5jb2RlIHRoZSBgYXJ0aXN0X3RvcF9nZW5yZWAgYXMgbnVtZXJpYyBidXQgd2UnbGwgZHJvcCBpdCBmb3Igbm93LgoKYGBge3Igc2VsZWN0X2NvbHVtbnN9CiMgU2VsZWN0IHZhcmlhYmxlcyB3aXRoIHNpbWlsYXIgcmFuZ2VzCmRmX251bWVyaWNfc2VsZWN0IDwtIGRmX251bWVyaWMgJT4lIAogIHNlbGVjdChwb3B1bGFyaXR5LCBkYW5jZWFiaWxpdHksIGFjb3VzdGljbmVzcywgbG91ZG5lc3MsIGVuZXJneSkgCgojIE5vcm1hbGl6ZSBkYXRhCiMgZGZfbnVtZXJpY19zZWxlY3QgPC0gc2NhbGUoZGZfbnVtZXJpY19zZWxlY3QpCmBgYAoKIyMgMy4gQ29tcHV0aW5nIGstbWVhbnMgY2x1c3RlcmluZyBpbiBSCgpXZSBjYW4gY29tcHV0ZSBrLW1lYW5zIGluIFIgd2l0aCB0aGUgYnVpbHQtaW4gYGttZWFuc2AgZnVuY3Rpb24sIHNlZSBgaGVscCgia21lYW5zKCkiKWAuIGBrbWVhbnMoKWAgZnVuY3Rpb24gYWNjZXB0cyBhIGRhdGEgZnJhbWUgd2l0aCBhbGwgbnVtZXJpYyBjb2x1bW5zIGFzIGl0J3MgcHJpbWFyeSBhcmd1bWVudC4KClRoZSBmaXJzdCBzdGVwIHdoZW4gdXNpbmcgay1tZWFucyBjbHVzdGVyaW5nIGlzIHRvIHNwZWNpZnkgdGhlIG51bWJlciBvZiBjbHVzdGVycyAoaykgdGhhdCB3aWxsIGJlIGdlbmVyYXRlZCBpbiB0aGUgZmluYWwgc29sdXRpb24uIFdlIGtub3cgdGhlcmUgYXJlIDMgc29uZyBnZW5yZXMgdGhhdCB3ZSBjYXJ2ZWQgb3V0IG9mIHRoZSBkYXRhc2V0LCBzbyBsZXQncyB0cnkgMzoKCmBgYHtyIGttZWFuc30Kc2V0LnNlZWQoMjA1NikKIyBLbWVhbnMgY2x1c3RlcmluZyBmb3IgMyBjbHVzdGVycwprY2x1c3QgPC0ga21lYW5zKAogIGRmX251bWVyaWNfc2VsZWN0LAogICMgU3BlY2lmeSB0aGUgbnVtYmVyIG9mIGNsdXN0ZXJzCiAgY2VudGVycyA9IDMsCiAgIyBIb3cgbWFueSByYW5kb20gaW5pdGlhbCBjb25maWd1cmF0aW9ucwogIG5zdGFydCA9IDI1CikKCiMgRGlzcGxheSBjbHVzdGVyaW5nIG9iamVjdAprY2x1c3QKYGBgCgpUaGUga21lYW5zIG9iamVjdCBjb250YWlucyBzZXZlcmFsIGJpdHMgb2YgaW5mb3JtYXRpb24gd2hpY2ggaXMgd2VsbCBleHBsYWluZWQgaW4gYGhlbHAoImttZWFucygpIilgLiBGb3Igbm93LCBsZXQncyBmb2N1cyBvbiBhIGZldy4gV2Ugc2VlIHRoYXQgdGhlIGRhdGEgaGFzIGJlZW4gZ3JvdXBlZCBpbnRvIDMgY2x1c3RlcnMgb2Ygc2l6ZXMgNjUsIDExMCwgMTExLiBUaGUgb3V0cHV0IGFsc28gY29udGFpbnMgdGhlIGNsdXN0ZXIgY2VudGVycyAobWVhbnMpIGZvciB0aGUgMyBncm91cHMgYWNyb3NzIHRoZSA1IHZhcmlhYmxlcy4KClRoZSBjbHVzdGVyaW5nIHZlY3RvciBpcyB0aGUgY2x1c3RlciBhc3NpZ25tZW50IGZvciBlYWNoIG9ic2VydmF0aW9uLiBMZXQncyB1c2UgdGhlIGBhdWdtZW50YCBmdW5jdGlvbiB0byBhZGQgdGhlIGNsdXN0ZXIgYXNzaWdubWVudCB0aGUgb3JpZ2luYWwgZGF0YSBzZXQuCgpgYGB7ciBhdWdtZW50fQojIEFkZCBwcmVkaWN0ZWQgY2x1c3RlciBhc3NpZ25tZW50IHRvIGRhdGEgc2V0CmF1Z21lbnQoa2NsdXN0LCBkZl9udW1lcmljX3NlbGVjdCkgJT4lIAogIHJlbG9jYXRlKC5jbHVzdGVyKSAlPiUgCiAgc2xpY2VfaGVhZChuID0gMTApCmBgYAoKUGVyZmVjdCwgd2UgaGF2ZSBqdXN0IHBhcnRpdGlvbmVkIG91ciBkYXRhIHNldCBpbnRvIGEgc2V0IG9mIDMgZ3JvdXBzLiBTbywgaG93IGdvb2QgaXMgb3VyIGNsdXN0ZXJpbmcg8J+ktz8gTGV0J3MgdGFrZSBhIGxvb2sgYXQgdGhlIGBTaWxob3VldHRlIHNjb3JlYAoKIyMjICoqU2lsaG91ZXR0ZSBzY29yZSoqCgpbU2lsaG91ZXR0ZSBhbmFseXNpc10oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvU2lsaG91ZXR0ZV8oY2x1c3RlcmluZykpIGNhbiBiZSB1c2VkIHRvIHN0dWR5IHRoZSBzZXBhcmF0aW9uIGRpc3RhbmNlIGJldHdlZW4gdGhlIHJlc3VsdGluZyBjbHVzdGVycy4gVGhpcyBzY29yZSB2YXJpZXMgZnJvbSAtMSB0byAxLCBhbmQgaWYgdGhlIHNjb3JlIGlzIG5lYXIgMSwgdGhlIGNsdXN0ZXIgaXMgZGVuc2UgYW5kIHdlbGwtc2VwYXJhdGVkIGZyb20gb3RoZXIgY2x1c3RlcnMuIEEgdmFsdWUgbmVhciAwIHJlcHJlc2VudHMgb3ZlcmxhcHBpbmcgY2x1c3RlcnMgd2l0aCBzYW1wbGVzIHZlcnkgY2xvc2UgdG8gdGhlIGRlY2lzaW9uIGJvdW5kYXJ5IG9mIHRoZSBuZWlnaGJvcmluZyBjbHVzdGVycy4gWyhTb3VyY2UpXShodHRwczovL2R6b25lLmNvbS9hcnRpY2xlcy9rbWVhbnMtc2lsaG91ZXR0ZS1zY29yZS1leHBsYWluZWQtd2l0aC1weXRob24tZXhhbSkuCgpUaGUgYXZlcmFnZSBzaWxob3VldHRlIG1ldGhvZCBjb21wdXRlcyB0aGUgYXZlcmFnZSBzaWxob3VldHRlIG9mIG9ic2VydmF0aW9ucyBmb3IgZGlmZmVyZW50IHZhbHVlcyBvZiAqayouIEEgaGlnaCBhdmVyYWdlIHNpbGhvdWV0dGUgc2NvcmUgaW5kaWNhdGVzIGEgZ29vZCBjbHVzdGVyaW5nLgoKVGhlIGBzaWxob3VldHRlYCBmdW5jdGlvbiBpbiB0aGUgY2x1c3RlciBwYWNrYWdlIHRvIGNvbXB1YXRlIHRoZSBhdmVyYWdlIHNpbGhvdWV0dGUgd2lkdGguCgo+IFRoZSBzaWxob3VldHRlIGNhbiBiZSBjYWxjdWxhdGVkIHdpdGggYW55IFtkaXN0YW5jZV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvRGlzdGFuY2UgIkRpc3RhbmNlIikgbWV0cmljLCBzdWNoIGFzIHRoZSBbRXVjbGlkZWFuIGRpc3RhbmNlXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9FdWNsaWRlYW5fZGlzdGFuY2UgIkV1Y2xpZGVhbiBkaXN0YW5jZSIpIG9yIHRoZSBbTWFuaGF0dGFuIGRpc3RhbmNlXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9NYW5oYXR0YW5fZGlzdGFuY2UgIk1hbmhhdHRhbiBkaXN0YW5jZSIpIHdoaWNoIHdlIGRpc2N1c3NlZCBpbiB0aGUgW3ByZXZpb3VzIGxlc3Nvbl0oaHR0cHM6Ly9naXRodWIuY29tL21pY3Jvc29mdC9NTC1Gb3ItQmVnaW5uZXJzL2Jsb2IvbWFpbi81LUNsdXN0ZXJpbmcvMS1WaXN1YWxpemUvc29sdXRpb24vUi9sZXNzb25fMTQtUi5pcHluYikuCgpgYGB7cn0KIyBMb2FkIGNsdXN0ZXIgcGFja2FnZQpsaWJyYXJ5KGNsdXN0ZXIpCgojIENvbXB1dGUgYXZlcmFnZSBzaWxob3VldHRlIHNjb3JlCnNzIDwtIHNpbGhvdWV0dGUoa2NsdXN0JGNsdXN0ZXIsCiAgICAgICAgICAgICAgICAgIyBDb21wdXRlIGV1Y2xpZGVhbiBkaXN0YW5jZQogICAgICAgICAgICAgICAgIGRpc3QgPSBkaXN0KGRmX251bWVyaWNfc2VsZWN0KSkKbWVhbihzc1ssIDNdKQoKYGBgCgpPdXIgc2NvcmUgaXMgKiouNTQ5KiosIHNvIHJpZ2h0IGluIHRoZSBtaWRkbGUuIFRoaXMgaW5kaWNhdGVzIHRoYXQgb3VyIGRhdGEgaXMgbm90IHBhcnRpY3VsYXJseSB3ZWxsLXN1aXRlZCB0byB0aGlzIHR5cGUgb2YgY2x1c3RlcmluZy4gTGV0J3Mgc2VlIHdoZXRoZXIgd2UgY2FuIGNvbmZpcm0gdGhpcyBodW5jaCB2aXN1YWxseS4gVGhlIFtmYWN0b2V4dHJhIHBhY2thZ2VdKGh0dHBzOi8vcnBrZ3MuZGF0YW5vdmlhLmNvbS9mYWN0b2V4dHJhL2luZGV4Lmh0bWwpIHByb3ZpZGVzIGZ1bmN0aW9ucyAoYGZ2aXpfY2x1c3RlcigpYCkgdG8gdmlzdWFsaXplIGNsdXN0ZXJpbmcuCgpgYGB7ciBmdml6X2NsdXN0ZXJ9CmxpYnJhcnkoZmFjdG9leHRyYSkKCiMgVmlzdWFsaXplIGNsdXN0ZXJpbmcgcmVzdWx0cwpmdml6X2NsdXN0ZXIoa2NsdXN0LCBkZl9udW1lcmljX3NlbGVjdCkKCmBgYAoKVGhlIG92ZXJsYXAgaW4gY2x1c3RlcnMgaW5kaWNhdGVzIHRoYXQgb3VyIGRhdGEgaXMgbm90IHBhcnRpY3VsYXJseSB3ZWxsLXN1aXRlZCB0byB0aGlzIHR5cGUgb2YgY2x1c3RlcmluZyBidXQgbGV0J3MgY29udGludWUuCgojIyA0LiBEZXRlcm1pbmluZyBvcHRpbWFsIGNsdXN0ZXJzCgpBIGZ1bmRhbWVudGFsIHF1ZXN0aW9uIHRoYXQgb2Z0ZW4gYXJpc2VzIGluIEstTWVhbnMgY2x1c3RlcmluZyBpcyB0aGlzIC0gd2l0aG91dCBrbm93biBjbGFzcyBsYWJlbHMsIGhvdyBkbyB5b3Uga25vdyBob3cgbWFueSBjbHVzdGVycyB0byBzZXBhcmF0ZSB5b3VyIGRhdGEgaW50bz8KCk9uZSB3YXkgd2UgY2FuIHRyeSB0byBmaW5kIG91dCBpcyB0byB1c2UgYSBkYXRhIHNhbXBsZSB0byBgY3JlYXRlIGEgc2VyaWVzIG9mIGNsdXN0ZXJpbmcgbW9kZWxzYCB3aXRoIGFuIGluY3JlbWVudGluZyBudW1iZXIgb2YgY2x1c3RlcnMgKGUuZyBmcm9tIDEtMTApLCBhbmQgZXZhbHVhdGUgY2x1c3RlcmluZyBtZXRyaWNzIHN1Y2ggYXMgdGhlICoqU2lsaG91ZXR0ZSBzY29yZS4qKgoKTGV0J3MgZGV0ZXJtaW5lIHRoZSBvcHRpbWFsIG51bWJlciBvZiBjbHVzdGVycyBieSBjb21wdXRpbmcgdGhlIGNsdXN0ZXJpbmcgYWxnb3JpdGhtIGZvciBkaWZmZXJlbnQgdmFsdWVzIG9mICprKiBhbmQgZXZhbHVhdGluZyB0aGUgKipXaXRoaW4gQ2x1c3RlciBTdW0gb2YgU3F1YXJlcyoqIChXQ1NTKS4gVGhlIHRvdGFsIHdpdGhpbi1jbHVzdGVyIHN1bSBvZiBzcXVhcmUgKFdDU1MpIG1lYXN1cmVzIHRoZSBjb21wYWN0bmVzcyBvZiB0aGUgY2x1c3RlcmluZyBhbmQgd2Ugd2FudCBpdCB0byBiZSBhcyBzbWFsbCBhcyBwb3NzaWJsZSwgd2l0aCBsb3dlciB2YWx1ZXMgbWVhbmluZyB0aGF0IHRoZSBkYXRhIHBvaW50cyBhcmUgY2xvc2VyLgoKTGV0J3MgZXhwbG9yZSB0aGUgZWZmZWN0IG9mIGRpZmZlcmVudCBjaG9pY2VzIG9mIGBrYCwgZnJvbSAxIHRvIDEwLCBvbiB0aGlzIGNsdXN0ZXJpbmcuCgpgYGB7cn0KIyBDcmVhdGUgYSBzZXJpZXMgb2YgY2x1c3RlcmluZyBtb2RlbHMKa2NsdXN0cyA8LSB0aWJibGUoayA9IDE6MTApICU+JSAKICAjIFBlcmZvcm0ga21lYW5zIGNsdXN0ZXJpbmcgZm9yIDEsMiwzIC4uLiAsMTAgY2x1c3RlcnMKICBtdXRhdGUobW9kZWwgPSBtYXAoaywgfiBrbWVhbnMoZGZfbnVtZXJpY19zZWxlY3QsIGNlbnRlcnMgPSAueCwgbnN0YXJ0ID0gMjUpKSwKICAjIEZhcm0gb3V0IGNsdXN0ZXJpbmcgbWV0cmljcyBlZyBXQ1NTCiAgICAgICAgIGdsYW5jZWQgPSBtYXAobW9kZWwsIH4gZ2xhbmNlKC54KSkpICU+JSAKICB1bm5lc3QoY29scyA9IGdsYW5jZWQpCiAgCgojIFZpZXcgY2x1c3RlcmluZyByc3Vsc3RzCmtjbHVzdHMKYGBgCgpOb3cgdGhhdCB3ZSBoYXZlIHRoZSB0b3RhbCB3aXRoaW4tY2x1c3RlciBzdW0tb2Ytc3F1YXJlcyAodG90LndpdGhpbnNzKSBmb3IgZWFjaCBjbHVzdGVyaW5nIGFsZ29yaXRobSB3aXRoIGNlbnRlciAqayosIHdlIHVzZSB0aGUgW2VsYm93IG1ldGhvZF0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvRWxib3dfbWV0aG9kXyhjbHVzdGVyaW5nKSkgdG8gZmluZCB0aGUgb3B0aW1hbCBudW1iZXIgb2YgY2x1c3RlcnMuIFRoZSBtZXRob2QgY29uc2lzdHMgb2YgcGxvdHRpbmcgdGhlIFdDU1MgYXMgYSBmdW5jdGlvbiBvZiB0aGUgbnVtYmVyIG9mIGNsdXN0ZXJzLCBhbmQgcGlja2luZyB0aGUgW2VsYm93IG9mIHRoZSBjdXJ2ZV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvRWxib3dfb2ZfdGhlX2N1cnZlICJFbGJvdyBvZiB0aGUgY3VydmUiKSBhcyB0aGUgbnVtYmVyIG9mIGNsdXN0ZXJzIHRvIHVzZS4KCmBgYHtyIGVsYm93X21ldGhvZH0Kc2V0LnNlZWQoMjA1NikKIyBVc2UgZWxib3cgbWV0aG9kIHRvIGRldGVybWluZSBvcHRpbXVtIG51bWJlciBvZiBjbHVzdGVycwprY2x1c3RzICU+JSAKICBnZ3Bsb3QobWFwcGluZyA9IGFlcyh4ID0gaywgeSA9IHRvdC53aXRoaW5zcykpICsKICBnZW9tX2xpbmUoc2l6ZSA9IDEuMiwgYWxwaGEgPSAwLjgsIGNvbG9yID0gIiNGRjdGMEVGRiIpICsKICBnZW9tX3BvaW50KHNpemUgPSAyLCBjb2xvciA9ICIjRkY3RjBFRkYiKQpgYGAKClRoZSBwbG90IHNob3dzIGEgbGFyZ2UgcmVkdWN0aW9uIGluIFdDU1MgKHNvIGdyZWF0ZXIgKnRpZ2h0bmVzcyopIGFzIHRoZSBudW1iZXIgb2YgY2x1c3RlcnMgaW5jcmVhc2VzIGZyb20gb25lIHRvIHR3bywgYW5kIGEgZnVydGhlciBub3RpY2VhYmxlIHJlZHVjdGlvbiBmcm9tIHR3byB0byB0aHJlZSBjbHVzdGVycy4gQWZ0ZXIgdGhhdCwgdGhlIHJlZHVjdGlvbiBpcyBsZXNzIHByb25vdW5jZWQsIHJlc3VsdGluZyBpbiBhbiBgZWxib3dgIPCfkqppbiB0aGUgY2hhcnQgYXQgYXJvdW5kIHRocmVlIGNsdXN0ZXJzLiBUaGlzIGlzIGEgZ29vZCBpbmRpY2F0aW9uIHRoYXQgdGhlcmUgYXJlIHR3byB0byB0aHJlZSByZWFzb25hYmx5IHdlbGwgc2VwYXJhdGVkIGNsdXN0ZXJzIG9mIGRhdGEgcG9pbnRzLgoKV2UgY2FuIG5vdyBnbyBhaGVhZCBhbmQgZXh0cmFjdCB0aGUgY2x1c3RlcmluZyBtb2RlbCB3aGVyZSBgayA9IDNgOgoKPiBgcHVsbCgpYDogdXNlZCB0byBleHRyYWN0IGEgc2luZ2xlIGNvbHVtbgo+Cj4gYHBsdWNrKClgOiB1c2VkIHRvIGluZGV4IGRhdGEgc3RydWN0dXJlcyBzdWNoIGFzIGxpc3RzCgpgYGB7ciBleHRyYWN0X21vZGVsfQojIEV4dHJhY3QgayA9IDMgY2x1c3RlcmluZwpmaW5hbF9rbWVhbnMgPC0ga2NsdXN0cyAlPiUgCiAgZmlsdGVyKGsgPT0gMykgJT4lIAogIHB1bGwobW9kZWwpICU+JSAKICBwbHVjaygxKQoKCmZpbmFsX2ttZWFucwpgYGAKCkdyZWF0ISBMZXQncyBnbyBhaGVhZCBhbmQgdmlzdWFsaXplIHRoZSBjbHVzdGVycyBvYnRhaW5lZC4gQ2FyZSBmb3Igc29tZSBpbnRlcmFjdGl2aXR5IHVzaW5nIGBwbG90bHlgPwoKYGBge3Igdml6X2NsdXN0fQojIEFkZCBwcmVkaWN0ZWQgY2x1c3RlciBhc3NpZ25tZW50IHRvIGRhdGEgc2V0CnJlc3VsdHMgPC0gIGF1Z21lbnQoZmluYWxfa21lYW5zLCBkZl9udW1lcmljX3NlbGVjdCkgJT4lIAogIGJpbmRfY29scyhkZl9udW1lcmljICU+JSBzZWxlY3QoYXJ0aXN0X3RvcF9nZW5yZSkpIAoKIyBQbG90IGNsdXN0ZXIgYXNzaWdubWVudHMKY2x1c3RfcGx0IDwtIHJlc3VsdHMgJT4lIAogIGdncGxvdChtYXBwaW5nID0gYWVzKHggPSBwb3B1bGFyaXR5LCB5ID0gZGFuY2VhYmlsaXR5LCBjb2xvciA9IC5jbHVzdGVyLCBzaGFwZSA9IGFydGlzdF90b3BfZ2VucmUpKSArCiAgZ2VvbV9wb2ludChzaXplID0gMiwgYWxwaGEgPSAwLjgpICsKICBwYWxldHRlZXI6OnNjYWxlX2NvbG9yX3BhbGV0dGVlcl9kKCJnZ3RoZW1lczo6VGFibGVhdV8xMCIpCgpnZ3Bsb3RseShjbHVzdF9wbHQpCgpgYGAKClBlcmhhcHMgd2Ugd291bGQgaGF2ZSBleHBlY3RlZCB0aGF0IGVhY2ggY2x1c3RlciAocmVwcmVzZW50ZWQgYnkgZGlmZmVyZW50IGNvbG9ycykgd291bGQgaGF2ZSBkaXN0aW5jdCBnZW5yZXMgKHJlcHJlc2VudGVkIGJ5IGRpZmZlcmVudCBzaGFwZXMpLgoKTGV0J3MgdGFrZSBhIGxvb2sgYXQgdGhlIG1vZGVsJ3MgYWNjdXJhY3kuCgpgYGB7ciBvcmRpbmFsX2VuY29kZX0KIyBBc3NpZ24gZ2VucmVzIHRvIHByZWRlZmluZWQgaW50ZWdlcnMKbGFiZWxfY291bnQgPC0gcmVzdWx0cyAlPiUgCiAgZ3JvdXBfYnkoYXJ0aXN0X3RvcF9nZW5yZSkgJT4lIAogIG11dGF0ZShpZCA9IGN1cl9ncm91cF9pZCgpKSAlPiUgCiAgdW5ncm91cCgpICU+JSAKICBzdW1tYXJpc2UoY29ycmVjdF9sYWJlbHMgPSBzdW0oLmNsdXN0ZXIgPT0gaWQpKQoKCiMgUHJpbnQgcmVzdWx0cyAgCmNhdCgiUmVzdWx0OiIsIGxhYmVsX2NvdW50JGNvcnJlY3RfbGFiZWxzLCAib3V0IG9mIiwgbnJvdyhyZXN1bHRzKSwgInNhbXBsZXMgd2VyZSBjb3JyZWN0bHkgbGFiZWxlZC4iKQoKY2F0KCJcbkFjY3VyYWN5IHNjb3JlOiIsIGxhYmVsX2NvdW50JGNvcnJlY3RfbGFiZWxzL25yb3cocmVzdWx0cykpCgpgYGAKClRoaXMgbW9kZWwncyBhY2N1cmFjeSBpcyBub3QgYmFkLCBidXQgbm90IGdyZWF0LiBJdCBtYXkgYmUgdGhhdCB0aGUgZGF0YSBtYXkgbm90IGxlbmQgaXRzZWxmIHdlbGwgdG8gSy1NZWFucyBDbHVzdGVyaW5nLiBUaGlzIGRhdGEgaXMgdG9vIGltYmFsYW5jZWQsIHRvbyBsaXR0bGUgY29ycmVsYXRlZCBhbmQgdGhlcmUgaXMgdG9vIG11Y2ggdmFyaWFuY2UgYmV0d2VlbiB0aGUgY29sdW1uIHZhbHVlcyB0byBjbHVzdGVyIHdlbGwuIEluIGZhY3QsIHRoZSBjbHVzdGVycyB0aGF0IGZvcm0gYXJlIHByb2JhYmx5IGhlYXZpbHkgaW5mbHVlbmNlZCBvciBza2V3ZWQgYnkgdGhlIHRocmVlIGdlbnJlIGNhdGVnb3JpZXMgd2UgZGVmaW5lZCBhYm92ZS4KCk5ldmVydGhlbGVzcywgdGhhdCB3YXMgcXVpdGUgYSBsZWFybmluZyBwcm9jZXNzIQoKSW4gU2Npa2l0LWxlYXJuJ3MgZG9jdW1lbnRhdGlvbiwgeW91IGNhbiBzZWUgdGhhdCBhIG1vZGVsIGxpa2UgdGhpcyBvbmUsIHdpdGggY2x1c3RlcnMgbm90IHZlcnkgd2VsbCBkZW1hcmNhdGVkLCBoYXMgYSAndmFyaWFuY2UnIHByb2JsZW06CgohW0luZm9ncmFwaGljIGZyb20gU2Npa2l0LWxlYXJuXSguLi8uLi9pbWFnZXMvcHJvYmxlbXMucG5nKQoKIyMgKipWYXJpYW5jZSoqCgpWYXJpYW5jZSBpcyBkZWZpbmVkIGFzICJ0aGUgYXZlcmFnZSBvZiB0aGUgc3F1YXJlZCBkaWZmZXJlbmNlcyBmcm9tIHRoZSBNZWFuIiBbKFNvdXJjZSldKGh0dHBzOi8vd3d3Lm1hdGhzaXNmdW4uY29tL2RhdGEvc3RhbmRhcmQtZGV2aWF0aW9uLmh0bWwpLiBJbiB0aGUgY29udGV4dCBvZiB0aGlzIGNsdXN0ZXJpbmcgcHJvYmxlbSwgaXQgcmVmZXJzIHRvIGRhdGEgdGhhdCB0aGUgbnVtYmVycyBvZiBvdXIgZGF0YXNldCB0ZW5kIHRvIGRpdmVyZ2UgYSBiaXQgdG9vIG11Y2ggZnJvbSB0aGUgbWVhbi4KCuKchSBUaGlzIGlzIGEgZ3JlYXQgbW9tZW50IHRvIHRoaW5rIGFib3V0IGFsbCB0aGUgd2F5cyB5b3UgY291bGQgY29ycmVjdCB0aGlzIGlzc3VlLiBUd2VhayB0aGUgZGF0YSBhIGJpdCBtb3JlPyBVc2UgZGlmZmVyZW50IGNvbHVtbnM/IFVzZSBhIGRpZmZlcmVudCBhbGdvcml0aG0/IEhpbnQ6IFRyeSBbc2NhbGluZyB5b3VyIGRhdGFdKGh0dHBzOi8vd3d3Lm15Z3JlYXRsZWFybmluZy5jb20vYmxvZy9sZWFybmluZy1kYXRhLXNjaWVuY2Utd2l0aC1rLW1lYW5zLWNsdXN0ZXJpbmcvKSB0byBub3JtYWxpemUgaXQgYW5kIHRlc3Qgb3RoZXIgY29sdW1ucy4KCj4gVHJ5IHRoaXMgJ1t2YXJpYW5jZSBjYWxjdWxhdG9yXShodHRwczovL3d3dy5jYWxjdWxhdG9yc291cC5jb20vY2FsY3VsYXRvcnMvc3RhdGlzdGljcy92YXJpYW5jZS1jYWxjdWxhdG9yLnBocCknIHRvIHVuZGVyc3RhbmQgdGhlIGNvbmNlcHQgYSBiaXQgbW9yZS4KCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKIyMgKirwn5qAQ2hhbGxlbmdlKioKClNwZW5kIHNvbWUgdGltZSB3aXRoIHRoaXMgbm90ZWJvb2ssIHR3ZWFraW5nIHBhcmFtZXRlcnMuIENhbiB5b3UgaW1wcm92ZSB0aGUgYWNjdXJhY3kgb2YgdGhlIG1vZGVsIGJ5IGNsZWFuaW5nIHRoZSBkYXRhIG1vcmUgKHJlbW92aW5nIG91dGxpZXJzLCBmb3IgZXhhbXBsZSk/IFlvdSBjYW4gdXNlIHdlaWdodHMgdG8gZ2l2ZSBtb3JlIHdlaWdodCB0byBnaXZlbiBkYXRhIHNhbXBsZXMuIFdoYXQgZWxzZSBjYW4geW91IGRvIHRvIGNyZWF0ZSBiZXR0ZXIgY2x1c3RlcnM/CgpIaW50OiBUcnkgdG8gc2NhbGUgeW91ciBkYXRhLiBUaGVyZSdzIGNvbW1lbnRlZCBjb2RlIGluIHRoZSBub3RlYm9vayB0aGF0IGFkZHMgc3RhbmRhcmQgc2NhbGluZyB0byBtYWtlIHRoZSBkYXRhIGNvbHVtbnMgcmVzZW1ibGUgZWFjaCBvdGhlciBtb3JlIGNsb3NlbHkgaW4gdGVybXMgb2YgcmFuZ2UuIFlvdSdsbCBmaW5kIHRoYXQgd2hpbGUgdGhlIHNpbGhvdWV0dGUgc2NvcmUgZ29lcyBkb3duLCB0aGUgJ2tpbmsnIGluIHRoZSBlbGJvdyBncmFwaCBzbW9vdGhzIG91dC4gVGhpcyBpcyBiZWNhdXNlIGxlYXZpbmcgdGhlIGRhdGEgdW5zY2FsZWQgYWxsb3dzIGRhdGEgd2l0aCBsZXNzIHZhcmlhbmNlIHRvIGNhcnJ5IG1vcmUgd2VpZ2h0LiBSZWFkIGEgYml0IG1vcmUgb24gdGhpcyBwcm9ibGVtIFtoZXJlXShodHRwczovL3N0YXRzLnN0YWNrZXhjaGFuZ2UuY29tL3F1ZXN0aW9ucy8yMTIyMi9hcmUtbWVhbi1ub3JtYWxpemF0aW9uLWFuZC1mZWF0dXJlLXNjYWxpbmctbmVlZGVkLWZvci1rLW1lYW5zLWNsdXN0ZXJpbmcvMjEyMjYjMjEyMjYpLgoKIyMgWyoqUG9zdC1sZWN0dXJlIHF1aXoqKl0oaHR0cHM6Ly9ncmF5LXNhbmQtMDdhMTBmNDAzLjEuYXp1cmVzdGF0aWNhcHBzLm5ldC9xdWl6LzMwLykKCiMjICoqUmV2aWV3ICYgU2VsZiBTdHVkeSoqCgotICAgVGFrZSBhIGxvb2sgYXQgYSBLLU1lYW5zIFNpbXVsYXRvciBbc3VjaCBhcyB0aGlzIG9uZV0oaHR0cHM6Ly91c2VyLmNlbmcubWV0dS5lZHUudHIvfmFraWZha2t1cy9jb3Vyc2VzL2Nlbmc1NzQvay1tZWFucy8pLiBZb3UgY2FuIHVzZSB0aGlzIHRvb2wgdG8gdmlzdWFsaXplIHNhbXBsZSBkYXRhIHBvaW50cyBhbmQgZGV0ZXJtaW5lIGl0cyBjZW50cm9pZHMuIFlvdSBjYW4gZWRpdCB0aGUgZGF0YSdzIHJhbmRvbW5lc3MsIG51bWJlcnMgb2YgY2x1c3RlcnMgYW5kIG51bWJlcnMgb2YgY2VudHJvaWRzLiBEb2VzIHRoaXMgaGVscCB5b3UgZ2V0IGFuIGlkZWEgb2YgaG93IHRoZSBkYXRhIGNhbiBiZSBncm91cGVkPwoKLSAgIEFsc28sIHRha2UgYSBsb29rIGF0IFt0aGlzIGhhbmRvdXQgb24gSy1NZWFuc10oaHR0cHM6Ly9zdGFuZm9yZC5lZHUvfmNwaWVjaC9jczIyMS9oYW5kb3V0cy9rbWVhbnMuaHRtbCkgZnJvbSBTdGFuZm9yZC4KCldhbnQgdG8gdHJ5IG91dCB5b3VyIG5ld2x5IGFjcXVpcmVkIGNsdXN0ZXJpbmcgc2tpbGxzIHRvIGRhdGEgc2V0cyB0aGF0IGxlbmQgd2VsbCB0byBLLU1lYW5zIGNsdXN0ZXJpbmc/IFBsZWFzZSBzZWU6CgotICAgW1RyYWluIGFuZCBFdmFsdWF0ZSBDbHVzdGVyaW5nIE1vZGVsc10oaHR0cHM6Ly9ycHVicy5jb20vZVJfaWMvY2x1c3RlcmluZykgdXNpbmcgVGlkeW1vZGVscyBhbmQgZnJpZW5kcwoKLSAgIFtLLW1lYW5zIENsdXN0ZXIgQW5hbHlzaXNdKGh0dHBzOi8vdWMtci5naXRodWIuaW8va21lYW5zX2NsdXN0ZXJpbmcpLCBVQyBCdXNpbmVzcyBBbmFseXRpY3MgUiBQcm9ncmFtbWluZyBHdWlkZQoKLSAgIFtLLW1lYW5zwqBjbHVzdGVyaW5nwqB3aXRowqB0aWR5wqBkYXRhwqBwcmluY2lwbGVzXShodHRwczovL3d3dy50aWR5bW9kZWxzLm9yZy9sZWFybi9zdGF0aXN0aWNzL2stbWVhbnMvKQoKIyMgKipBc3NpZ25tZW50KioKCltUcnkgZGlmZmVyZW50IGNsdXN0ZXJpbmcgbWV0aG9kc10oaHR0cHM6Ly9naXRodWIuY29tL21pY3Jvc29mdC9NTC1Gb3ItQmVnaW5uZXJzL2Jsb2IvbWFpbi81LUNsdXN0ZXJpbmcvMi1LLU1lYW5zL2Fzc2lnbm1lbnQubWQpCgojIyBUSEFOSyBZT1UgVE86CgpbSmVuIExvb3Blcl0oaHR0cHM6Ly93d3cudHdpdHRlci5jb20vamVubG9vcGVyKSBmb3IgY3JlYXRpbmcgdGhlIG9yaWdpbmFsIFB5dGhvbiB2ZXJzaW9uIG9mIHRoaXMgbW9kdWxlIOKZpe+4jwoKW2BBbGxpc29uIEhvcnN0YF0oaHR0cHM6Ly90d2l0dGVyLmNvbS9hbGxpc29uX2hvcnN0LykgZm9yIGNyZWF0aW5nIHRoZSBhbWF6aW5nIGlsbHVzdHJhdGlvbnMgdGhhdCBtYWtlIFIgbW9yZSB3ZWxjb21pbmcgYW5kIGVuZ2FnaW5nLiBGaW5kIG1vcmUgaWxsdXN0cmF0aW9ucyBhdCBoZXIgW2dhbGxlcnldKGh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vdXJsP3E9aHR0cHM6Ly9naXRodWIuY29tL2FsbGlzb25ob3JzdC9zdGF0cy1pbGx1c3RyYXRpb25zJnNhPUQmc291cmNlPWVkaXRvcnMmdXN0PTE2MjYzODA3NzI1MzAwMDAmdXNnPUFPdlZhdzN6Y2Z5Q2l6RlFacGtTTHp4aWlRRU0pLgoKSGFwcHkgTGVhcm5pbmcsCgpbRXJpY10oaHR0cHM6Ly90d2l0dGVyLmNvbS9lcmljbnRheSksIEdvbGQgTWljcm9zb2Z0IExlYXJuIFN0dWRlbnQgQW1iYXNzYWRvci4KCiFbQXJ0d29yayBieSBcQGFsbGlzb25faG9yc3RdKC4uLy4uL2ltYWdlcy9yX2xlYXJuZXJzX3NtLmpwZWcpCgojYGBge3IgaW5jbHVkZT1GQUxTRX0KI2xpYnJhcnkoaGVyZSkKI2xpYnJhcnkocm1kMmp1cHl0ZXIpCiNybWQyanVweXRlcigibGVzc29uXzE0LlJtZCIpCiNgYGAK