In this example, we are going to learn how to make a recommendation system using collaborative filtering. Collaborative filtering is one of the most common approaches used to recommend products or services to customers and became very popular after the famous Netflix competition. By creating a collaborative filtering algorithm with keras, you will also be exposed to how we can create more customized models with keras’ functional model options.

Learning objectives:

  • How to create a neural network collaborative filtering algorithm
  • How to create a customized functional keras model

Requirements

library(keras)
library(tidyverse)
library(glue)

Prepare our data

For this module we’ll use MovieLens data, which provides user rating information for movies. There are multiple dataset sizes; however, for efficiency we will use the smaller dataset that contains 100,836 ratings of 9,724 movies rated by 610 users.

# get path to data
data_dir <- here::here("materials", "data", "ml-latest-small")

movies <- read_csv(file.path(data_dir, "movies.csv"))
ratings <- read_csv(file.path(data_dir, "ratings.csv"))

Currently our datasets are separate and movie ID ranges from 1 to 193,609 even though our data only contains 9,724 unique movie IDs. Consequently, the following:

  1. creates a dense_movie_id so there are no gaps, which makes future mapping of our word vector to embeddings simpler,
  2. joins our datasets,
  3. cleans up our column names,
  4. and converts our IDs to be zero-based (makes things easier).
movie_data <- ratings %>% 
  distinct(movieId) %>%
  rowid_to_column(var = "dense_movie_id") %>%
  inner_join(ratings) %>%
  inner_join(movies) %>%
  select(user_id = userId, movie_id = movieId, dense_movie_id, rating, everything()) %>%
  mutate(user_id = user_id - 1, dense_movie_id = dense_movie_id - 1)

movie_data

Let’s extract the number of movies and users. We’ll use these parameters later in our keras model.

n_movies <- n_distinct(movie_data$dense_movie_id)
n_users <- n_distinct(movie_data$user_id)

glue("This dataset includes {nrow(movie_data)} ratings by {n_users} users on {n_movies} unique movies")
This dataset includes 100836 ratings by 610 users on 9724 unique movies

Lastly, let’s randomize our data and then create our feature and response tensors. Note that our feature set simply contains the user and movie ID.

set.seed(123)
movie_data <- movie_data %>% sample_frac()

x_train <- movie_data %>% select(c(user_id, dense_movie_id)) %>% as.matrix()
y_train <- movie_data %>% pull(rating)

head(x_train)
     user_id dense_movie_id
[1,]      62           1159
[2,]     159           1413
[3,]     468             24
[4,]     473            561
[5,]     596           6168
[6,]     297           1958

Create a collaborative filtering algorithm

Collaborative filtering is a general concept and there are several algorithms to implement it. Here is a good article that discusses the different types but they can loosely be categorized as:

  • Distance-based (i.e. cosine similarity, correlation)
  • Matrix factorization (ℹ️)
  • Clustering
  • Deep learning

Our method will implement a neural network approach.

Blended learning

Fast.ai povides some fantastic virtual training that is worth exploring. Two videos you should watch now include:

  • lesson 4 min 1:07:25 - 1:17:00 & 1:22:00-1:24:00
  • lesson 5 min 0:20:00 - 0:39:00

Embeddings

One of the first things we need to do is select the dimension of the embeddings that we will use for users and movies. As with word embeddings, the dimension of our embeddings is a tunable hyperparameter. For now, we’ll use 64.

embedding_dim <- 64

Basic model

To build our model, we need to take a different approach than the traditional keras_model_sequential() approach. Instead we need to build a model that resembles this:

First, let’s create our input and embedding layers. We create an input and embedding for our user IDs and our movie IDs. Since each of these inputs are a single dimension we specify shape = 1 in our layer_input().

Our embedding layers build onto each of these inputs:

  • input_dim: number of unique user and movie IDs
  • output_dim: represents the desired embeddings dimension (64 in this example).
# input layers
input_users <- layer_input(shape = 1, name = "users")
input_movies <- layer_input(shape = 1, name = "movies")

user_embeddings <- input_users %>% 
  layer_embedding(
    input_dim = n_users,
    output_dim = embedding_dim,
    name = "user_embeddings"
  ) 

movie_embeddings <- input_movies %>% 
  layer_embedding(
    input_dim = n_movies,
    output_dim = embedding_dim,
    name = "movie_embeddings"
  ) 

Recall from our Excel example, we multiplied the user embeddings by the movie embeddings. This is referred to as a dot product and we can use layer_dot() to execute this computation. Since our embeddings outputs are matrices we want to perform a dot product with the embedding columns (axes = 2). If our outputs were vectors we would use axes = 1.

We add our final prediction layer with layer_dense(). Since our predicted rating can’t be < 0 I use activation = "relu" rather than a purely linear activation.

dot <- layer_dot(
  inputs = list(user_embeddings, movie_embeddings),
  axes = 2,
  name = "dot_product"
  )

pred <- dot %>% layer_dense(
  units = 1, 
  activation = "relu",
  name = "rating_prediction"
  )

Now, we just need to combine these layers into a keras model. We use keras_model() to do so and we specify our 2 input layers and map them to our output layer. We can then add our compilation information as usual.

Note how our model summary illustrates how our layers are connected together.

# define model inputs/outputs
model <- keras_model(inputs = c(input_users, input_movies), outputs = pred)

model %>% compile(
  optimizer = "rmsprop",
  loss = "mse",
  metric = "mae"
)

# inspect model
summary(model)
Model: "model"
____________________________________________________________________________________
Layer (type)               Output Shape       Param #   Connected to                
====================================================================================
users (InputLayer)         [(None, 1)]        0                                     
____________________________________________________________________________________
movies (InputLayer)        [(None, 1)]        0                                     
____________________________________________________________________________________
user_embeddings (Embedding (None, 1, 64)      39040     users[0][0]                 
____________________________________________________________________________________
movie_embeddings (Embeddin (None, 1, 64)      622336    movies[0][0]                
____________________________________________________________________________________
dot_product (Dot)          (None, 1, 1)       0         user_embeddings[0][0]       
                                                        movie_embeddings[0][0]      
____________________________________________________________________________________
rating_prediction (Dense)  (None, 1, 1)       2         dot_product[0][0]           
====================================================================================
Total params: 661,378
Trainable params: 661,378
Non-trainable params: 0
____________________________________________________________________________________

We are now ready to train our model. The only difference in this step is since we have two different input layers (input_users & input_movies), we need to supply a list of two inputs:

  • x_train[, "user_id", drop = FALSE]: tensor (matrix) of user IDs
  • x_train[, "dense_movie_id", drop = FALSE]: tensor (matrix) of movie IDs
# train the model
history <- model %>% fit(
  x = list(
    x_train[, "user_id", drop = FALSE],
    x_train[, "dense_movie_id", drop = FALSE]
  ),
  y = y_train,
  epochs = 10,
  batch_size = 32, 
  validation_split = 0.2,
  callbacks = list(callback_early_stopping(patience = 2))
)

Our model obtains a loss in the lower 0.8 range.

best_epoch <- which(history$metrics$val_loss == min(history$metrics$val_loss))
loss <- history$metrics$val_loss[best_epoch] %>% round(3)
mae <- history$metrics$val_mae[best_epoch] %>% round(3)

glue("The best epoch had a loss of {loss} and mean absolute error of {mae}")
The best epoch had a loss of 0.818 and mean absolute error of 0.698

Accounting for bias

Unfortunately, our simple model does not account for biases. For example, some people tend to rate everything favorably and some movies are consistently highly rated. We can capture this extra information by including extra bias weights in our model ℹ️.

Doing this results in a neural net architecture that looks like:

We follow the same procedure as before to set up the user and movie embeddings. We also create two new bias layers (user_bias & movie_bias) that will have an output dimension of 1 since this is creating a single bias weight for each user and movie.

# input layers
input_users <- layer_input(shape = 1, name = "users")
input_movies <- layer_input(shape = 1, name = "movies")

user_embeddings <- input_users %>%
  layer_embedding(
    input_dim = n_users,
    output_dim = embedding_dim,
    name = "user_embeddings"
  )

movie_embeddings <- input_movies %>%
  layer_embedding(
    input_dim = n_movies,
    output_dim = embedding_dim,
    name = "movie_embeddings"
  )

user_bias <- input_users %>%
  layer_embedding(
    input_dim = n_users,
    output_dim = 1,
    name = "user_bias"
  ) 

movie_bias <- input_users %>%
  layer_embedding(
    input_dim = n_movies,
    output_dim = 1,
    name = "movie_bias"
  ) 

We create our dot product and then add one more layer that adds the dot product with the user and movie biases (via layer_add()). We then complete our model with our final prediction layer.

dot <- layer_dot(list(user_embeddings, movie_embeddings), axes = 2, 
                 name = "dot_product")

dot_bias <- layer_add(list(dot, user_bias, movie_bias), name = "add_bias")

pred <- dot_bias %>% layer_dense(units = 1, activation = "relu", 
                                 name = "rating_prediction")

We follow the same procedure to build our model with keras_model() and then compile. Our model summary shows our new layers that include, or are connected to, our biases.

# define model inputs/outputs
model <- keras_model(inputs = c(input_users, input_movies), outputs = pred)

model %>% compile(
  optimizer = "rmsprop",
  loss = "mse",
  metric = "mae"
)

# inspect model
summary(model)
Model: "model_1"
____________________________________________________________________________________
Layer (type)               Output Shape       Param #   Connected to                
====================================================================================
users (InputLayer)         [(None, 1)]        0                                     
____________________________________________________________________________________
movies (InputLayer)        [(None, 1)]        0                                     
____________________________________________________________________________________
user_embeddings (Embedding (None, 1, 64)      39040     users[0][0]                 
____________________________________________________________________________________
movie_embeddings (Embeddin (None, 1, 64)      622336    movies[0][0]                
____________________________________________________________________________________
dot_product (Dot)          (None, 1, 1)       0         user_embeddings[0][0]       
                                                        movie_embeddings[0][0]      
____________________________________________________________________________________
user_bias (Embedding)      (None, 1, 1)       610       users[0][0]                 
____________________________________________________________________________________
movie_bias (Embedding)     (None, 1, 1)       9724      users[0][0]                 
____________________________________________________________________________________
add_bias (Add)             (None, 1, 1)       0         dot_product[0][0]           
                                                        user_bias[0][0]             
                                                        movie_bias[0][0]            
____________________________________________________________________________________
rating_prediction (Dense)  (None, 1, 1)       2         add_bias[0][0]              
====================================================================================
Total params: 671,712
Trainable params: 671,712
Non-trainable params: 0
____________________________________________________________________________________

We train our model the same way as before:

# train the model
history <- model %>% fit(
  x = list(
    x_train[, "user_id", drop = FALSE],
    x_train[, "dense_movie_id", drop = FALSE]
  ),
  y = y_train,
  epochs = 10,
  batch_size = 32, 
  validation_split = 0.2,
  callbacks = list(callback_early_stopping(patience = 2))
)

Our results show an improvement of over 5 percentage points! Spending some time on hyperparameter optimization could very well lead to even better results.

best_epoch <- which(history$metrics$val_loss == min(history$metrics$val_loss))
loss <- history$metrics$val_loss[best_epoch] %>% round(3)
mae <- history$metrics$val_mae[best_epoch] %>% round(3)

glue("The best epoch had a loss of {loss} and mean absolute error of {mae}")
The best epoch had a loss of 0.756 and mean absolute error of 0.669

A closer look at the embeddings

If we wanted to take a closer look at our beddings we can always access them. For example, let’s grab the movie embeddings:

movie_embeddings <- model %>%
  get_layer("movie_embeddings") %>% 
  get_weights() %>%
  .[[1]]

The following just adds the actual movie titles to the embeddings after some regex clean up to remove unncessary info. Note that the movie embeddings are ordered based on the dense_movie_id value (i.e. 1, 2, …, n) so we need to properly order the titles before adding them as row names.

movie_titles <- movie_data %>%
  select(dense_movie_id, title) %>%
  distinct() %>%
  arrange(dense_movie_id) %>%
  mutate(title = title %>% str_remove("\\(.+\\)") %>% str_trim())

row.names(movie_embeddings) <- movie_titles$title

movie_embeddings[1:10, 1:4]
                            [,1]         [,2]         [,3]         [,4]
Toy Story           -0.074543737  0.208834410 -0.029036991  0.076254986
Grumpier Old Men     0.064601205 -0.004361281  0.060442824 -0.049045525
Heat                 0.036556825 -0.040174905  0.097080767  0.142855868
Seven                0.095864780 -0.049991976 -0.005479141  0.025697527
Usual Suspects, The -0.103922434  0.158935905 -0.234381482  0.136216059
From Dusk Till Dawn -0.005881237  0.002334971  0.070011735  0.076987430
Bottle Rocket       -0.016652498 -0.023864822 -0.009117431 -0.059774224
Braveheart           0.164630473 -0.044428393 -0.062731601  0.042318888
Rob Roy             -0.106735967  0.088052034 -0.072279885  0.005268715
Canadian Bacon      -0.049028948 -0.065250292 -0.002858121  0.032802433

We can now use some kind of dimension reduction procedure. The following applies TSNe to group our movie embeddings along two dimensions and then plot them. If you zoom in you will see some clear themes among the groupings (i.e. Billy Madison, The Wedding Singer, Dumb & Dumber, Austin Powers are similar comedies).

n_words_to_plot <- 200

tsne <- Rtsne::Rtsne(
  X = movie_embeddings[1:n_words_to_plot,], 
  perplexity = 30, 
  pca = FALSE
  )

p <- tsne$Y %>%
  as.data.frame() %>%
  mutate(word = row.names(movie_embeddings)[1:n_words_to_plot]) %>%
  ggplot(aes(x = V1, y = V2, label = word)) + 
  geom_text(size = 3)

plotly::ggplotly(p)

You could do a similar process to find similar groupings of customers.

Make a customer prediction

Now that we have a model, we often want to make recommendations to customers about new products we think they’d like. For example, let’s look at customer 53. The following does some data wrangling to identify the movies that user 53 has and has not watched.

We can use this info to recommend a movie to this customer that we think they would enjoy but have not watched yet.

# convert customer of interest to align to our zero-based customer IDs
original_customer_id <- 53
new_customer_id <- original_customer_id - 1

# get movies watched by our user
movies_watched <- movie_data %>%
  filter(user_id == new_customer_id) %>% 
  pull(dense_movie_id)

# get all available movies
all_movies <- movie_data %>% 
  distinct(dense_movie_id) %>%
  pull()

# identify movies not watched
movies_not_watched <- setdiff(all_movies, movies_watched)

movie_options <- movie_data %>%
  filter(dense_movie_id %in% movies_not_watched) %>%
  distinct(dense_movie_id, title)

movie_options

To do so, we create a new matrix that includes the user’s zero-based index ID. In this example we can see this column is always “52” since we are only focusing on this one user. We then add a second column of all the dense_movie_ids for the movies that the user has not watched.

customer_options <- expand.grid(
  user_id = new_customer_id, 
  dense_movie_id = movies_not_watched
  ) %>%
  as.matrix()

head(customer_options)
     user_id dense_movie_id
[1,]      52           1159
[2,]      52           1413
[3,]      52             24
[4,]      52            561
[5,]      52           6168
[6,]      52           1958

We can now feed this information into our predict() function. Remember, our keras model takes two inputs (user_id & dense_movie_id) so our predict() function is going to expect a list of two inputs as well.

inputs <- list(
  customer_options[, "user_id", drop = FALSE],
  customer_options[, "dense_movie_id", drop = FALSE]
  )

pred <- model %>% predict(inputs)

head(pred)
, , 1

         [,1]
[1,] 4.281264
[2,] 2.949849
[3,] 3.871279
[4,] 4.101624
[5,] 3.548330
[6,] 4.059011

We can now add these predictions to our customer_options data, join the movie_options dataset that has the titles for the movies and rank-order our movies for those that have the highest expected rating.

customer_options %>%
  as_tibble() %>%
  mutate(predictions = as.vector(pred)) %>%
  left_join(movie_options, by = "dense_movie_id") %>%
  arrange(desc(predictions))

Key takeaways

  • Collaborative filtering
    • A common and relatively simple approach to make recommendations
    • There are many algorithms to choose from but matrix factorization and our deep learning extension is probably the most common.
    • All we’re doing is
      1. creating embeddings for both our users and products
      2. dot product multiplies these matrices of embeddings
      3. use additional bias weights to account for user/product biases
      4. and we can extend this with typical deep learning layers (i.e. hidden layers, dropout, etc.)
  • Keras functional model
    • Allows us flexibility in creating custom models
    • We can have multiple inputs (and subsequent layers) along with multiple outputs
    • Naming our layers allows us to easily view the layer connections
    • For more information on keras’ functional model see:

🏠

LS0tCnRpdGxlOiAiTW92aWUgcmVjb21tZW5kYXRpb25zIHdpdGggY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmciCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogdHJ1ZQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFKQpnZ3Bsb3QyOjp0aGVtZV9zZXQoZ2dwbG90Mjo6dGhlbWVfYncoKSkKYGBgCgpJbiB0aGlzIGV4YW1wbGUsIHdlIGFyZSBnb2luZyB0byBsZWFybiBob3cgdG8gbWFrZSBhIHJlY29tbWVuZGF0aW9uIHN5c3RlbSB1c2luZwpjb2xsYWJvcmF0aXZlIGZpbHRlcmluZy4gQ29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcgaXMgb25lIG9mIHRoZSBtb3N0IGNvbW1vbgphcHByb2FjaGVzIHVzZWQgdG8gcmVjb21tZW5kIHByb2R1Y3RzIG9yIHNlcnZpY2VzIHRvIGN1c3RvbWVycyBhbmQgYmVjYW1lIHZlcnkKcG9wdWxhciBhZnRlciB0aGUgZmFtb3VzIFtOZXRmbGl4IGNvbXBldGl0aW9uXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9OZXRmbGl4X1ByaXplKS4KQnkgY3JlYXRpbmcgYSBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyBhbGdvcml0aG0gd2l0aCBrZXJhcywgeW91IHdpbGwgYWxzbyBiZQpleHBvc2VkIHRvIGhvdyB3ZSBjYW4gY3JlYXRlIG1vcmUgY3VzdG9taXplZCBtb2RlbHMgd2l0aCBrZXJhcycgZnVuY3Rpb25hbAptb2RlbCBvcHRpb25zLgoKTGVhcm5pbmcgb2JqZWN0aXZlczoKCi0gSG93IHRvIGNyZWF0ZSBhIG5ldXJhbCBuZXR3b3JrIGNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nIGFsZ29yaXRobQotIEhvdyB0byBjcmVhdGUgYSBjdXN0b21pemVkIGZ1bmN0aW9uYWwga2VyYXMgbW9kZWwKCiMgUmVxdWlyZW1lbnRzCgpgYGB7cn0KbGlicmFyeShrZXJhcykKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoZ2x1ZSkKYGBgCgojIFByZXBhcmUgb3VyIGRhdGEKCkZvciB0aGlzIG1vZHVsZSB3ZSdsbCB1c2UgW01vdmllTGVucyBkYXRhXShodHRwczovL2dyb3VwbGVucy5vcmcvZGF0YXNldHMvbW92aWVsZW5zLyksCndoaWNoIHByb3ZpZGVzIHVzZXIgcmF0aW5nIGluZm9ybWF0aW9uIGZvciBtb3ZpZXMuIFRoZXJlIGFyZSBtdWx0aXBsZSBkYXRhc2V0CnNpemVzOyBob3dldmVyLCBmb3IgZWZmaWNpZW5jeSB3ZSB3aWxsIHVzZSB0aGUgc21hbGxlciBkYXRhc2V0IHRoYXQgY29udGFpbnMKMTAwLDgzNiByYXRpbmdzIG9mIDksNzI0IG1vdmllcyByYXRlZCBieSA2MTAgdXNlcnMuCgpgYGB7cn0KIyBnZXQgcGF0aCB0byBkYXRhCmRhdGFfZGlyIDwtIGhlcmU6OmhlcmUoIm1hdGVyaWFscyIsICJkYXRhIiwgIm1sLWxhdGVzdC1zbWFsbCIpCgptb3ZpZXMgPC0gcmVhZF9jc3YoZmlsZS5wYXRoKGRhdGFfZGlyLCAibW92aWVzLmNzdiIpKQpyYXRpbmdzIDwtIHJlYWRfY3N2KGZpbGUucGF0aChkYXRhX2RpciwgInJhdGluZ3MuY3N2IikpCmBgYAoKQ3VycmVudGx5IG91ciBkYXRhc2V0cyBhcmUgc2VwYXJhdGUgYW5kIG1vdmllIElEIHJhbmdlcyBmcm9tIDEgdG8gMTkzLDYwOSBldmVuCnRob3VnaCBvdXIgZGF0YSBvbmx5IGNvbnRhaW5zIDksNzI0IHVuaXF1ZSBtb3ZpZSBJRHMuIENvbnNlcXVlbnRseSwgdGhlIGZvbGxvd2luZzoKCjEuIGNyZWF0ZXMgYSBgZGVuc2VfbW92aWVfaWRgIHNvIHRoZXJlIGFyZSBubyBnYXBzLCB3aGljaCBtYWtlcyBmdXR1cmUgbWFwcGluZwogICBvZiBvdXIgd29yZCB2ZWN0b3IgdG8gZW1iZWRkaW5ncyBzaW1wbGVyLAoyLiBqb2lucyBvdXIgZGF0YXNldHMsCjMuIGNsZWFucyB1cCBvdXIgY29sdW1uIG5hbWVzLAo0LiBhbmQgY29udmVydHMgb3VyIElEcyB0byBiZSB6ZXJvLWJhc2VkIChtYWtlcyB0aGluZ3MgZWFzaWVyKS4KCmBgYHtyfQptb3ZpZV9kYXRhIDwtIHJhdGluZ3MgJT4lIAogIGRpc3RpbmN0KG1vdmllSWQpICU+JQogIHJvd2lkX3RvX2NvbHVtbih2YXIgPSAiZGVuc2VfbW92aWVfaWQiKSAlPiUKICBpbm5lcl9qb2luKHJhdGluZ3MpICU+JQogIGlubmVyX2pvaW4obW92aWVzKSAlPiUKICBzZWxlY3QodXNlcl9pZCA9IHVzZXJJZCwgbW92aWVfaWQgPSBtb3ZpZUlkLCBkZW5zZV9tb3ZpZV9pZCwgcmF0aW5nLCBldmVyeXRoaW5nKCkpICU+JQogIG11dGF0ZSh1c2VyX2lkID0gdXNlcl9pZCAtIDEsIGRlbnNlX21vdmllX2lkID0gZGVuc2VfbW92aWVfaWQgLSAxKQoKbW92aWVfZGF0YQpgYGAKCkxldCdzIGV4dHJhY3QgdGhlIG51bWJlciBvZiBtb3ZpZXMgYW5kIHVzZXJzLiBXZSdsbCB1c2UgdGhlc2UgcGFyYW1ldGVycyBsYXRlcgppbiBvdXIga2VyYXMgbW9kZWwuCgpgYGB7cn0Kbl9tb3ZpZXMgPC0gbl9kaXN0aW5jdChtb3ZpZV9kYXRhJGRlbnNlX21vdmllX2lkKQpuX3VzZXJzIDwtIG5fZGlzdGluY3QobW92aWVfZGF0YSR1c2VyX2lkKQoKZ2x1ZSgiVGhpcyBkYXRhc2V0IGluY2x1ZGVzIHtucm93KG1vdmllX2RhdGEpfSByYXRpbmdzIGJ5IHtuX3VzZXJzfSB1c2VycyBvbiB7bl9tb3ZpZXN9IHVuaXF1ZSBtb3ZpZXMiKQpgYGAKCkxhc3RseSwgbGV0J3MgcmFuZG9taXplIG91ciBkYXRhIGFuZCB0aGVuIGNyZWF0ZSBvdXIgZmVhdHVyZSBhbmQgcmVzcG9uc2UKdGVuc29ycy4gTm90ZSB0aGF0IG91ciBmZWF0dXJlIHNldCBzaW1wbHkgY29udGFpbnMgdGhlIHVzZXIgYW5kIG1vdmllIElELgoKYGBge3J9CnNldC5zZWVkKDEyMykKbW92aWVfZGF0YSA8LSBtb3ZpZV9kYXRhICU+JSBzYW1wbGVfZnJhYygpCgp4X3RyYWluIDwtIG1vdmllX2RhdGEgJT4lIHNlbGVjdChjKHVzZXJfaWQsIGRlbnNlX21vdmllX2lkKSkgJT4lIGFzLm1hdHJpeCgpCnlfdHJhaW4gPC0gbW92aWVfZGF0YSAlPiUgcHVsbChyYXRpbmcpCgpoZWFkKHhfdHJhaW4pCmBgYAoKIyBDcmVhdGUgYSBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyBhbGdvcml0aG0KCkNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nIGlzIGEgZ2VuZXJhbCBjb25jZXB0IGFuZCB0aGVyZSBhcmUgc2V2ZXJhbCBhbGdvcml0aG1zIHRvCmltcGxlbWVudCBpdC4gSGVyZSBpcyBhIGdvb2QgW2FydGljbGVdKGh0dHBzOi8vYml0Lmx5LzM0c1FWOGcpIHRoYXQgZGlzY3Vzc2VzCnRoZSBkaWZmZXJlbnQgdHlwZXMgYnV0IHRoZXkgY2FuIGxvb3NlbHkgYmUgY2F0ZWdvcml6ZWQgYXM6CgoqIERpc3RhbmNlLWJhc2VkIChpLmUuIGNvc2luZSBzaW1pbGFyaXR5LCBjb3JyZWxhdGlvbikKKiBNYXRyaXggZmFjdG9yaXphdGlvbiAoW+KEue+4j10oaHR0cHM6Ly9naXRodWIuY29tL21pc2stZGF0YS1zY2llbmNlL21pc2stZGwvdHJlZS9tYXN0ZXIvbWF0ZXJpYWxzLzA3LXJlY29tbWVuZGVyLWNvbGxhYm9yYXRpdmUtZmlsdGVyaW5nKSkKKiBDbHVzdGVyaW5nCiogRGVlcCBsZWFybmluZwoKT3VyIG1ldGhvZCB3aWxsIGltcGxlbWVudCBhIG5ldXJhbCBuZXR3b3JrIGFwcHJvYWNoLgoKIyMgQmxlbmRlZCBsZWFybmluZwoKW0Zhc3QuYWldKGh0dHBzOi8vd3d3LmZhc3QuYWkvKSBwb3ZpZGVzIHNvbWUgZmFudGFzdGljIHZpcnR1YWwgdHJhaW5pbmcgdGhhdCBpcyB3b3J0aCBleHBsb3JpbmcuIFR3byB2aWRlb3MgeW91IHNob3VsZCB3YXRjaCBub3cgaW5jbHVkZToKCi0gW2xlc3NvbiA0XShodHRwczovL3lvdXR1LmJlL0M5VWRWUEUzeW5BP3Q9MWg3bTI1cykgbWluIDE6MDc6MjUgLSAxOjE3OjAwICYgMToyMjowMC0xOjI0OjAwCi0gW2xlc3NvbiA1XShodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD90PTE5bTUwcyZlZHVmaWx0ZXI9TlVMTCZmZWF0dXJlPXlvdXR1LmJlJnY9dVF0VHdocHY3RXcpIG1pbiAwOjIwOjAwIC0gMDozOTowMAoKIyMgRW1iZWRkaW5ncwoKT25lIG9mIHRoZSBmaXJzdCB0aGluZ3Mgd2UgbmVlZCB0byBkbyBpcyBzZWxlY3QgdGhlIGRpbWVuc2lvbiBvZiB0aGUgZW1iZWRkaW5ncwp0aGF0IHdlIHdpbGwgdXNlIGZvciB1c2VycyBhbmQgbW92aWVzLiBBcyB3aXRoIHdvcmQgZW1iZWRkaW5ncywgdGhlIGRpbWVuc2lvbiBvZgpvdXIgZW1iZWRkaW5ncyBpcyBhIHR1bmFibGUgaHlwZXJwYXJhbWV0ZXIuIEZvciBub3csIHdlJ2xsIHVzZSA2NC4KCmBgYHtyfQplbWJlZGRpbmdfZGltIDwtIDY0CmBgYAoKIyMgQmFzaWMgbW9kZWwKClRvIGJ1aWxkIG91ciBtb2RlbCwgd2UgbmVlZCB0byB0YWtlIGEgZGlmZmVyZW50IGFwcHJvYWNoIHRoYW4gdGhlIHRyYWRpdGlvbmFsCmBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKClgIGFwcHJvYWNoLiBJbnN0ZWFkIHdlIG5lZWQgdG8gYnVpbGQgYSBtb2RlbCB0aGF0CnJlc2VtYmxlcyB0aGlzOgoKCiFbXSguLi9pbWFnZXMvY29sbGFib3JhdGl2ZS1maWx0ZXJpbmcta2VyYXMtbW9kZWwucG5nKQoKCkZpcnN0LCBsZXQncyBjcmVhdGUgb3VyIGlucHV0IGFuZCBlbWJlZGRpbmcgbGF5ZXJzLiBXZSBjcmVhdGUgYW4gaW5wdXQgYW5kCmVtYmVkZGluZyBmb3Igb3VyIHVzZXIgSURzIGFuZCBvdXIgbW92aWUgSURzLiBTaW5jZSBlYWNoIG9mIHRoZXNlIGlucHV0cyBhcmUgYQpzaW5nbGUgZGltZW5zaW9uIHdlIHNwZWNpZnkgYHNoYXBlID0gMWAgaW4gb3VyIGBsYXllcl9pbnB1dCgpYC4KCk91ciBlbWJlZGRpbmcgbGF5ZXJzIGJ1aWxkIG9udG8gZWFjaCBvZiB0aGVzZSBpbnB1dHM6CgotIGBpbnB1dF9kaW1gOiBudW1iZXIgb2YgdW5pcXVlIHVzZXIgYW5kIG1vdmllIElEcwotIGBvdXRwdXRfZGltYDogcmVwcmVzZW50cyB0aGUgZGVzaXJlZCBlbWJlZGRpbmdzIGRpbWVuc2lvbiAoNjQgaW4gdGhpcyBleGFtcGxlKS4KCmBgYHtyfQojIGlucHV0IGxheWVycwppbnB1dF91c2VycyA8LSBsYXllcl9pbnB1dChzaGFwZSA9IDEsIG5hbWUgPSAidXNlcnMiKQppbnB1dF9tb3ZpZXMgPC0gbGF5ZXJfaW5wdXQoc2hhcGUgPSAxLCBuYW1lID0gIm1vdmllcyIpCgp1c2VyX2VtYmVkZGluZ3MgPC0gaW5wdXRfdXNlcnMgJT4lIAogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IG5fdXNlcnMsCiAgICBvdXRwdXRfZGltID0gZW1iZWRkaW5nX2RpbSwKICAgIG5hbWUgPSAidXNlcl9lbWJlZGRpbmdzIgogICkgCgptb3ZpZV9lbWJlZGRpbmdzIDwtIGlucHV0X21vdmllcyAlPiUgCiAgbGF5ZXJfZW1iZWRkaW5nKAogICAgaW5wdXRfZGltID0gbl9tb3ZpZXMsCiAgICBvdXRwdXRfZGltID0gZW1iZWRkaW5nX2RpbSwKICAgIG5hbWUgPSAibW92aWVfZW1iZWRkaW5ncyIKICApIApgYGAKClJlY2FsbCBmcm9tIG91ciBFeGNlbCBleGFtcGxlLCB3ZSBtdWx0aXBsaWVkIHRoZSB1c2VyIGVtYmVkZGluZ3MgYnkgdGhlIG1vdmllCmVtYmVkZGluZ3MuIFRoaXMgaXMgcmVmZXJyZWQgdG8gYXMgYSBkb3QgcHJvZHVjdCBhbmQgd2UgY2FuIHVzZSBgbGF5ZXJfZG90KClgIHRvCmV4ZWN1dGUgdGhpcyBjb21wdXRhdGlvbi4gU2luY2Ugb3VyIGVtYmVkZGluZ3Mgb3V0cHV0cyBhcmUgbWF0cmljZXMgd2Ugd2FudCB0bwpwZXJmb3JtIGEgZG90IHByb2R1Y3Qgd2l0aCB0aGUgZW1iZWRkaW5nIGNvbHVtbnMgKGBheGVzID0gMmApLiBJZiBvdXIgb3V0cHV0cwp3ZXJlIHZlY3RvcnMgd2Ugd291bGQgdXNlIGBheGVzID0gMWAuCgpXZSBhZGQgb3VyIGZpbmFsIHByZWRpY3Rpb24gbGF5ZXIgd2l0aCBgbGF5ZXJfZGVuc2UoKWAuIFNpbmNlIG91ciBwcmVkaWN0ZWQKcmF0aW5nIGNhbid0IGJlIDwgMCBJIHVzZSBgYWN0aXZhdGlvbiA9ICJyZWx1ImAgcmF0aGVyIHRoYW4gYSBwdXJlbHkgbGluZWFyCmFjdGl2YXRpb24uCgpgYGB7cn0KZG90IDwtIGxheWVyX2RvdCgKICBpbnB1dHMgPSBsaXN0KHVzZXJfZW1iZWRkaW5ncywgbW92aWVfZW1iZWRkaW5ncyksCiAgYXhlcyA9IDIsCiAgbmFtZSA9ICJkb3RfcHJvZHVjdCIKICApCgpwcmVkIDwtIGRvdCAlPiUgbGF5ZXJfZGVuc2UoCiAgdW5pdHMgPSAxLCAKICBhY3RpdmF0aW9uID0gInJlbHUiLAogIG5hbWUgPSAicmF0aW5nX3ByZWRpY3Rpb24iCiAgKQpgYGAKCk5vdywgd2UganVzdCBuZWVkIHRvIGNvbWJpbmUgdGhlc2UgbGF5ZXJzIGludG8gYSBrZXJhcyBtb2RlbC4gV2UgdXNlCmBrZXJhc19tb2RlbCgpYCB0byBkbyBzbyBhbmQgd2Ugc3BlY2lmeSBvdXIgMiBpbnB1dCBsYXllcnMgYW5kIG1hcCB0aGVtIHRvIG91cgpvdXRwdXQgbGF5ZXIuIFdlIGNhbiB0aGVuIGFkZCBvdXIgY29tcGlsYXRpb24gaW5mb3JtYXRpb24gYXMgdXN1YWwuCgpOb3RlIGhvdyBvdXIgbW9kZWwgc3VtbWFyeSBpbGx1c3RyYXRlcyBob3cgb3VyIGxheWVycyBhcmUgY29ubmVjdGVkIHRvZ2V0aGVyLgoKYGBge3J9CiMgZGVmaW5lIG1vZGVsIGlucHV0cy9vdXRwdXRzCm1vZGVsIDwtIGtlcmFzX21vZGVsKGlucHV0cyA9IGMoaW5wdXRfdXNlcnMsIGlucHV0X21vdmllcyksIG91dHB1dHMgPSBwcmVkKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLAogIGxvc3MgPSAibXNlIiwKICBtZXRyaWMgPSAibWFlIgopCgojIGluc3BlY3QgbW9kZWwKc3VtbWFyeShtb2RlbCkKYGBgCgpXZSBhcmUgbm93IHJlYWR5IHRvIHRyYWluIG91ciBtb2RlbC4gVGhlIG9ubHkgZGlmZmVyZW5jZSBpbiB0aGlzIHN0ZXAgaXMgc2luY2UKd2UgaGF2ZSB0d28gZGlmZmVyZW50IGlucHV0IGxheWVycyAoYGlucHV0X3VzZXJzYCAmIGBpbnB1dF9tb3ZpZXNgKSwgd2UgbmVlZCB0bwpzdXBwbHkgYSBsaXN0IG9mIHR3byBpbnB1dHM6CgotIGB4X3RyYWluWywgInVzZXJfaWQiLCBkcm9wID0gRkFMU0VdYDogdGVuc29yIChtYXRyaXgpIG9mIHVzZXIgSURzCi0gYHhfdHJhaW5bLCAiZGVuc2VfbW92aWVfaWQiLCBkcm9wID0gRkFMU0VdYDogdGVuc29yIChtYXRyaXgpIG9mIG1vdmllIElEcwoKYGBge3J9CiMgdHJhaW4gdGhlIG1vZGVsCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4ID0gbGlzdCgKICAgIHhfdHJhaW5bLCAidXNlcl9pZCIsIGRyb3AgPSBGQUxTRV0sCiAgICB4X3RyYWluWywgImRlbnNlX21vdmllX2lkIiwgZHJvcCA9IEZBTFNFXQogICksCiAgeSA9IHlfdHJhaW4sCiAgZXBvY2hzID0gMTAsCiAgYmF0Y2hfc2l6ZSA9IDMyLCAKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yLAogIGNhbGxiYWNrcyA9IGxpc3QoY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSAyKSkKKQpgYGAKCk91ciBtb2RlbCBvYnRhaW5zIGEgbG9zcyBpbiB0aGUgbG93ZXIgMC44IHJhbmdlLgoKYGBge3J9CmJlc3RfZXBvY2ggPC0gd2hpY2goaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzID09IG1pbihoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3MpKQpsb3NzIDwtIGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zc1tiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKbWFlIDwtIGhpc3RvcnkkbWV0cmljcyR2YWxfbWFlW2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQoKZ2x1ZSgiVGhlIGJlc3QgZXBvY2ggaGFkIGEgbG9zcyBvZiB7bG9zc30gYW5kIG1lYW4gYWJzb2x1dGUgZXJyb3Igb2Yge21hZX0iKQpgYGAKCiMjIEFjY291bnRpbmcgZm9yIGJpYXMKClVuZm9ydHVuYXRlbHksIG91ciBzaW1wbGUgbW9kZWwgZG9lcyBub3QgYWNjb3VudCBmb3IgYmlhc2VzLiBGb3IgZXhhbXBsZSwgc29tZQpwZW9wbGUgdGVuZCB0byByYXRlIGV2ZXJ5dGhpbmcgZmF2b3JhYmx5IGFuZCBzb21lIG1vdmllcyBhcmUgY29uc2lzdGVudGx5IGhpZ2hseQpyYXRlZC4gV2UgY2FuIGNhcHR1cmUgdGhpcyBleHRyYSBpbmZvcm1hdGlvbiBieSBpbmNsdWRpbmcgZXh0cmEgYmlhcyB3ZWlnaHRzIGluCm91ciBtb2RlbCBb4oS577iPXShodHRwczovL2dpdGh1Yi5jb20vbWlzay1kYXRhLXNjaWVuY2UvbWlzay1kbC90cmVlL21hc3Rlci9tYXRlcmlhbHMvMDctcmVjb21tZW5kZXItY29sbGFib3JhdGl2ZS1maWx0ZXJpbmcpLgoKRG9pbmcgdGhpcyByZXN1bHRzIGluIGEgbmV1cmFsIG5ldCBhcmNoaXRlY3R1cmUgdGhhdCBsb29rcyBsaWtlOgoKIVtdKC4uL2ltYWdlcy9jb2xsYWJvcmF0aXZlLWZpbHRlcmluZy1rZXJhcy1tb2RlbDIucG5nKQoKV2UgZm9sbG93IHRoZSBzYW1lIHByb2NlZHVyZSBhcyBiZWZvcmUgdG8gc2V0IHVwIHRoZSB1c2VyIGFuZCBtb3ZpZSBlbWJlZGRpbmdzLgpXZSBhbHNvIGNyZWF0ZSB0d28gbmV3IGJpYXMgbGF5ZXJzIChgdXNlcl9iaWFzYCAmIGBtb3ZpZV9iaWFzYCkgdGhhdCB3aWxsIGhhdmUKYW4gb3V0cHV0IGRpbWVuc2lvbiBvZiAxIHNpbmNlIHRoaXMgaXMgY3JlYXRpbmcgYSBzaW5nbGUgYmlhcyB3ZWlnaHQgZm9yIGVhY2gKdXNlciBhbmQgbW92aWUuCgpgYGB7cn0KIyBpbnB1dCBsYXllcnMKaW5wdXRfdXNlcnMgPC0gbGF5ZXJfaW5wdXQoc2hhcGUgPSAxLCBuYW1lID0gInVzZXJzIikKaW5wdXRfbW92aWVzIDwtIGxheWVyX2lucHV0KHNoYXBlID0gMSwgbmFtZSA9ICJtb3ZpZXMiKQoKdXNlcl9lbWJlZGRpbmdzIDwtIGlucHV0X3VzZXJzICU+JQogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IG5fdXNlcnMsCiAgICBvdXRwdXRfZGltID0gZW1iZWRkaW5nX2RpbSwKICAgIG5hbWUgPSAidXNlcl9lbWJlZGRpbmdzIgogICkKCm1vdmllX2VtYmVkZGluZ3MgPC0gaW5wdXRfbW92aWVzICU+JQogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IG5fbW92aWVzLAogICAgb3V0cHV0X2RpbSA9IGVtYmVkZGluZ19kaW0sCiAgICBuYW1lID0gIm1vdmllX2VtYmVkZGluZ3MiCiAgKQoKdXNlcl9iaWFzIDwtIGlucHV0X3VzZXJzICU+JQogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IG5fdXNlcnMsCiAgICBvdXRwdXRfZGltID0gMSwKICAgIG5hbWUgPSAidXNlcl9iaWFzIgogICkgCgptb3ZpZV9iaWFzIDwtIGlucHV0X3VzZXJzICU+JQogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IG5fbW92aWVzLAogICAgb3V0cHV0X2RpbSA9IDEsCiAgICBuYW1lID0gIm1vdmllX2JpYXMiCiAgKSAKYGBgCgpXZSBjcmVhdGUgb3VyIGRvdCBwcm9kdWN0IGFuZCB0aGVuIGFkZCBvbmUgbW9yZSBsYXllciB0aGF0IGFkZHMgdGhlIGRvdCBwcm9kdWN0CndpdGggdGhlIHVzZXIgYW5kIG1vdmllIGJpYXNlcyAodmlhIGBsYXllcl9hZGQoKWApLiBXZSB0aGVuIGNvbXBsZXRlIG91ciBtb2RlbAp3aXRoIG91ciBmaW5hbCBwcmVkaWN0aW9uIGxheWVyLgoKYGBge3J9CmRvdCA8LSBsYXllcl9kb3QobGlzdCh1c2VyX2VtYmVkZGluZ3MsIG1vdmllX2VtYmVkZGluZ3MpLCBheGVzID0gMiwgCiAgICAgICAgICAgICAgICAgbmFtZSA9ICJkb3RfcHJvZHVjdCIpCgpkb3RfYmlhcyA8LSBsYXllcl9hZGQobGlzdChkb3QsIHVzZXJfYmlhcywgbW92aWVfYmlhcyksIG5hbWUgPSAiYWRkX2JpYXMiKQoKcHJlZCA8LSBkb3RfYmlhcyAlPiUgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInJlbHUiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmFtZSA9ICJyYXRpbmdfcHJlZGljdGlvbiIpCmBgYAoKV2UgZm9sbG93IHRoZSBzYW1lIHByb2NlZHVyZSB0byBidWlsZCBvdXIgbW9kZWwgd2l0aCBga2VyYXNfbW9kZWwoKWAgYW5kIHRoZW4KY29tcGlsZS4gT3VyIG1vZGVsIHN1bW1hcnkgc2hvd3Mgb3VyIG5ldyBsYXllcnMgdGhhdCBpbmNsdWRlLCBvciBhcmUgY29ubmVjdGVkCnRvLCBvdXIgYmlhc2VzLgoKYGBge3J9CiMgZGVmaW5lIG1vZGVsIGlucHV0cy9vdXRwdXRzCm1vZGVsIDwtIGtlcmFzX21vZGVsKGlucHV0cyA9IGMoaW5wdXRfdXNlcnMsIGlucHV0X21vdmllcyksIG91dHB1dHMgPSBwcmVkKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLAogIGxvc3MgPSAibXNlIiwKICBtZXRyaWMgPSAibWFlIgopCgojIGluc3BlY3QgbW9kZWwKc3VtbWFyeShtb2RlbCkKYGBgCgpXZSB0cmFpbiBvdXIgbW9kZWwgdGhlIHNhbWUgd2F5IGFzIGJlZm9yZToKCmBgYHtyfQojIHRyYWluIHRoZSBtb2RlbApoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoCiAgeCA9IGxpc3QoCiAgICB4X3RyYWluWywgInVzZXJfaWQiLCBkcm9wID0gRkFMU0VdLAogICAgeF90cmFpblssICJkZW5zZV9tb3ZpZV9pZCIsIGRyb3AgPSBGQUxTRV0KICApLAogIHkgPSB5X3RyYWluLAogIGVwb2NocyA9IDEwLAogIGJhdGNoX3NpemUgPSAzMiwgCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwKICBjYWxsYmFja3MgPSBsaXN0KGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMikpCikKYGBgCgpPdXIgcmVzdWx0cyBzaG93IGFuIGltcHJvdmVtZW50IG9mIG92ZXIgNSBwZXJjZW50YWdlIHBvaW50cyEgU3BlbmRpbmcgc29tZSB0aW1lCm9uIGh5cGVycGFyYW1ldGVyIG9wdGltaXphdGlvbiBjb3VsZCB2ZXJ5IHdlbGwgbGVhZCB0byBldmVuIGJldHRlciByZXN1bHRzLgoKYGBge3J9CmJlc3RfZXBvY2ggPC0gd2hpY2goaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzID09IG1pbihoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3MpKQpsb3NzIDwtIGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zc1tiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKbWFlIDwtIGhpc3RvcnkkbWV0cmljcyR2YWxfbWFlW2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQoKZ2x1ZSgiVGhlIGJlc3QgZXBvY2ggaGFkIGEgbG9zcyBvZiB7bG9zc30gYW5kIG1lYW4gYWJzb2x1dGUgZXJyb3Igb2Yge21hZX0iKQpgYGAKCiMgQSBjbG9zZXIgbG9vayBhdCB0aGUgZW1iZWRkaW5ncwoKSWYgd2Ugd2FudGVkIHRvIHRha2UgYSBjbG9zZXIgbG9vayBhdCBvdXIgYmVkZGluZ3Mgd2UgY2FuIGFsd2F5cyBhY2Nlc3MgdGhlbS4KRm9yIGV4YW1wbGUsIGxldCdzIGdyYWIgdGhlIG1vdmllIGVtYmVkZGluZ3M6CgpgYGB7cn0KbW92aWVfZW1iZWRkaW5ncyA8LSBtb2RlbCAlPiUKICBnZXRfbGF5ZXIoIm1vdmllX2VtYmVkZGluZ3MiKSAlPiUgCiAgZ2V0X3dlaWdodHMoKSAlPiUKICAuW1sxXV0KYGBgCgpUaGUgZm9sbG93aW5nIGp1c3QgYWRkcyB0aGUgYWN0dWFsIG1vdmllIHRpdGxlcyB0byB0aGUgZW1iZWRkaW5ncyBhZnRlciBzb21lCnJlZ2V4IGNsZWFuIHVwIHRvIHJlbW92ZSB1bm5jZXNzYXJ5IGluZm8uIE5vdGUgdGhhdCB0aGUgbW92aWUgZW1iZWRkaW5ncyBhcmUKb3JkZXJlZCBiYXNlZCBvbiB0aGUgYGRlbnNlX21vdmllX2lkYCB2YWx1ZSAoaS5lLiAxLCAyLCAuLi4sIG4pIHNvIHdlIG5lZWQgdG8KcHJvcGVybHkgb3JkZXIgdGhlIHRpdGxlcyBiZWZvcmUgYWRkaW5nIHRoZW0gYXMgcm93IG5hbWVzLgoKYGBge3J9Cm1vdmllX3RpdGxlcyA8LSBtb3ZpZV9kYXRhICU+JQogIHNlbGVjdChkZW5zZV9tb3ZpZV9pZCwgdGl0bGUpICU+JQogIGRpc3RpbmN0KCkgJT4lCiAgYXJyYW5nZShkZW5zZV9tb3ZpZV9pZCkgJT4lCiAgbXV0YXRlKHRpdGxlID0gdGl0bGUgJT4lIHN0cl9yZW1vdmUoIlxcKC4rXFwpIikgJT4lIHN0cl90cmltKCkpCgpyb3cubmFtZXMobW92aWVfZW1iZWRkaW5ncykgPC0gbW92aWVfdGl0bGVzJHRpdGxlCgptb3ZpZV9lbWJlZGRpbmdzWzE6MTAsIDE6NF0KYGBgCgpXZSBjYW4gbm93IHVzZSBzb21lIGtpbmQgb2YgZGltZW5zaW9uIHJlZHVjdGlvbiBwcm9jZWR1cmUuIFRoZSBmb2xsb3dpbmcgYXBwbGllcwpUU05lIHRvIGdyb3VwIG91ciBtb3ZpZSBlbWJlZGRpbmdzIGFsb25nIHR3byBkaW1lbnNpb25zIGFuZCB0aGVuIHBsb3QgdGhlbS4gSWYKeW91IHpvb20gaW4geW91IHdpbGwgc2VlIHNvbWUgY2xlYXIgdGhlbWVzIGFtb25nIHRoZSBncm91cGluZ3MgKGkuZS4gQmlsbHkKTWFkaXNvbiwgVGhlIFdlZGRpbmcgU2luZ2VyLCBEdW1iICYgRHVtYmVyLCBBdXN0aW4gUG93ZXJzIGFyZSBzaW1pbGFyIGNvbWVkaWVzKS4KCmBgYHtyLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9Nn0Kbl93b3Jkc190b19wbG90IDwtIDIwMAoKdHNuZSA8LSBSdHNuZTo6UnRzbmUoCiAgWCA9IG1vdmllX2VtYmVkZGluZ3NbMTpuX3dvcmRzX3RvX3Bsb3QsXSwgCiAgcGVycGxleGl0eSA9IDMwLCAKICBwY2EgPSBGQUxTRQogICkKCnAgPC0gdHNuZSRZICU+JQogIGFzLmRhdGEuZnJhbWUoKSAlPiUKICBtdXRhdGUod29yZCA9IHJvdy5uYW1lcyhtb3ZpZV9lbWJlZGRpbmdzKVsxOm5fd29yZHNfdG9fcGxvdF0pICU+JQogIGdncGxvdChhZXMoeCA9IFYxLCB5ID0gVjIsIGxhYmVsID0gd29yZCkpICsgCiAgZ2VvbV90ZXh0KHNpemUgPSAzKQoKcGxvdGx5OjpnZ3Bsb3RseShwKQpgYGAKCllvdSBjb3VsZCBkbyBhIHNpbWlsYXIgcHJvY2VzcyB0byBmaW5kIHNpbWlsYXIgZ3JvdXBpbmdzIG9mIGN1c3RvbWVycy4KCiMgTWFrZSBhIGN1c3RvbWVyIHByZWRpY3Rpb24KCk5vdyB0aGF0IHdlIGhhdmUgYSBtb2RlbCwgd2Ugb2Z0ZW4gd2FudCB0byBtYWtlIHJlY29tbWVuZGF0aW9ucyB0byBjdXN0b21lcnMKYWJvdXQgbmV3IHByb2R1Y3RzIHdlIHRoaW5rIHRoZXknZCBsaWtlLiBGb3IgZXhhbXBsZSwgbGV0J3MgbG9vayBhdCBjdXN0b21lciA1My4KVGhlIGZvbGxvd2luZyBkb2VzIHNvbWUgZGF0YSB3cmFuZ2xpbmcgdG8gaWRlbnRpZnkgdGhlIG1vdmllcyB0aGF0IHVzZXIgNTMgaGFzCmFuZCBoYXMgbm90IHdhdGNoZWQuIAoKV2UgY2FuIHVzZSB0aGlzIGluZm8gdG8gcmVjb21tZW5kIGEgbW92aWUgdG8gdGhpcyBjdXN0b21lcgp0aGF0IHdlIHRoaW5rIHRoZXkgd291bGQgZW5qb3kgYnV0IGhhdmUgbm90IHdhdGNoZWQgeWV0LgoKYGBge3J9CiMgY29udmVydCBjdXN0b21lciBvZiBpbnRlcmVzdCB0byBhbGlnbiB0byBvdXIgemVyby1iYXNlZCBjdXN0b21lciBJRHMKb3JpZ2luYWxfY3VzdG9tZXJfaWQgPC0gNTMKbmV3X2N1c3RvbWVyX2lkIDwtIG9yaWdpbmFsX2N1c3RvbWVyX2lkIC0gMQoKIyBnZXQgbW92aWVzIHdhdGNoZWQgYnkgb3VyIHVzZXIKbW92aWVzX3dhdGNoZWQgPC0gbW92aWVfZGF0YSAlPiUKICBmaWx0ZXIodXNlcl9pZCA9PSBuZXdfY3VzdG9tZXJfaWQpICU+JSAKICBwdWxsKGRlbnNlX21vdmllX2lkKQoKIyBnZXQgYWxsIGF2YWlsYWJsZSBtb3ZpZXMKYWxsX21vdmllcyA8LSBtb3ZpZV9kYXRhICU+JSAKICBkaXN0aW5jdChkZW5zZV9tb3ZpZV9pZCkgJT4lCiAgcHVsbCgpCgojIGlkZW50aWZ5IG1vdmllcyBub3Qgd2F0Y2hlZAptb3ZpZXNfbm90X3dhdGNoZWQgPC0gc2V0ZGlmZihhbGxfbW92aWVzLCBtb3ZpZXNfd2F0Y2hlZCkKCm1vdmllX29wdGlvbnMgPC0gbW92aWVfZGF0YSAlPiUKICBmaWx0ZXIoZGVuc2VfbW92aWVfaWQgJWluJSBtb3ZpZXNfbm90X3dhdGNoZWQpICU+JQogIGRpc3RpbmN0KGRlbnNlX21vdmllX2lkLCB0aXRsZSkKCm1vdmllX29wdGlvbnMKYGBgCgpUbyBkbyBzbywgd2UgY3JlYXRlIGEgbmV3IG1hdHJpeCB0aGF0IGluY2x1ZGVzIHRoZSB1c2VyJ3MgemVyby1iYXNlZCBpbmRleCBJRC4KSW4gdGhpcyBleGFtcGxlIHdlIGNhbiBzZWUgdGhpcyBjb2x1bW4gaXMgYWx3YXlzICI1MiIgc2luY2Ugd2UgYXJlIG9ubHkgZm9jdXNpbmcKb24gdGhpcyBvbmUgdXNlci4gV2UgdGhlbiBhZGQgYSBzZWNvbmQgY29sdW1uIG9mIGFsbCB0aGUgYGRlbnNlX21vdmllX2lkYHMgZm9yCnRoZSBtb3ZpZXMgdGhhdCB0aGUgdXNlciBoYXMgbm90IHdhdGNoZWQuCgpgYGB7cn0KY3VzdG9tZXJfb3B0aW9ucyA8LSBleHBhbmQuZ3JpZCgKICB1c2VyX2lkID0gbmV3X2N1c3RvbWVyX2lkLCAKICBkZW5zZV9tb3ZpZV9pZCA9IG1vdmllc19ub3Rfd2F0Y2hlZAogICkgJT4lCiAgYXMubWF0cml4KCkKCmhlYWQoY3VzdG9tZXJfb3B0aW9ucykKYGBgCgpXZSBjYW4gbm93IGZlZWQgdGhpcyBpbmZvcm1hdGlvbiBpbnRvIG91ciBgcHJlZGljdCgpYCBmdW5jdGlvbi4gUmVtZW1iZXIsIG91cgprZXJhcyBtb2RlbCB0YWtlcyB0d28gaW5wdXRzIChgdXNlcl9pZGAgJiBgZGVuc2VfbW92aWVfaWRgKSBzbyBvdXIgYHByZWRpY3QoKWAKZnVuY3Rpb24gaXMgZ29pbmcgdG8gZXhwZWN0IGEgbGlzdCBvZiB0d28gaW5wdXRzIGFzIHdlbGwuCgpgYGB7cn0KaW5wdXRzIDwtIGxpc3QoCiAgY3VzdG9tZXJfb3B0aW9uc1ssICJ1c2VyX2lkIiwgZHJvcCA9IEZBTFNFXSwKICBjdXN0b21lcl9vcHRpb25zWywgImRlbnNlX21vdmllX2lkIiwgZHJvcCA9IEZBTFNFXQogICkKCnByZWQgPC0gbW9kZWwgJT4lIHByZWRpY3QoaW5wdXRzKQoKaGVhZChwcmVkKQpgYGAKCldlIGNhbiBub3cgYWRkIHRoZXNlIHByZWRpY3Rpb25zIHRvIG91ciBgY3VzdG9tZXJfb3B0aW9uc2AgZGF0YSwgam9pbiB0aGUKYG1vdmllX29wdGlvbnNgIGRhdGFzZXQgdGhhdCBoYXMgdGhlIHRpdGxlcyBmb3IgdGhlIG1vdmllcyBhbmQgcmFuay1vcmRlciBvdXIKbW92aWVzIGZvciB0aG9zZSB0aGF0IGhhdmUgdGhlIGhpZ2hlc3QgZXhwZWN0ZWQgcmF0aW5nLgoKYGBge3J9CmN1c3RvbWVyX29wdGlvbnMgJT4lCiAgYXNfdGliYmxlKCkgJT4lCiAgbXV0YXRlKHByZWRpY3Rpb25zID0gYXMudmVjdG9yKHByZWQpKSAlPiUKICBsZWZ0X2pvaW4obW92aWVfb3B0aW9ucywgYnkgPSAiZGVuc2VfbW92aWVfaWQiKSAlPiUKICBhcnJhbmdlKGRlc2MocHJlZGljdGlvbnMpKQpgYGAKCiMgS2V5IHRha2Vhd2F5cwoKKiBDb2xsYWJvcmF0aXZlIGZpbHRlcmluZwogICAtIEEgY29tbW9uIGFuZCByZWxhdGl2ZWx5IHNpbXBsZSBhcHByb2FjaCB0byBtYWtlIHJlY29tbWVuZGF0aW9ucwogICAtIFRoZXJlIGFyZSBtYW55IGFsZ29yaXRobXMgdG8gY2hvb3NlIGZyb20gYnV0IG1hdHJpeCBmYWN0b3JpemF0aW9uIGFuZCBvdXIKICAgICBkZWVwIGxlYXJuaW5nIGV4dGVuc2lvbiBpcyBwcm9iYWJseSB0aGUgbW9zdCBjb21tb24uCiAgIC0gQWxsIHdlJ3JlIGRvaW5nIGlzIAogICAgICAxLiBjcmVhdGluZyBlbWJlZGRpbmdzIGZvciBib3RoIG91ciB1c2VycyBhbmQgcHJvZHVjdHMKICAgICAgMi4gZG90IHByb2R1Y3QgbXVsdGlwbGllcyB0aGVzZSBtYXRyaWNlcyBvZiBlbWJlZGRpbmdzCiAgICAgIDMuIHVzZSBhZGRpdGlvbmFsIGJpYXMgd2VpZ2h0cyB0byBhY2NvdW50IGZvciB1c2VyL3Byb2R1Y3QgYmlhc2VzCiAgICAgIDQuIGFuZCB3ZSBjYW4gZXh0ZW5kIHRoaXMgd2l0aCB0eXBpY2FsIGRlZXAgbGVhcm5pbmcgbGF5ZXJzIChpLmUuIGhpZGRlbgogICAgICAgICBsYXllcnMsIGRyb3BvdXQsIGV0Yy4pCiogS2VyYXMgZnVuY3Rpb25hbCBtb2RlbAogICAtIEFsbG93cyB1cyBmbGV4aWJpbGl0eSBpbiBjcmVhdGluZyBjdXN0b20gbW9kZWxzCiAgIC0gV2UgY2FuIGhhdmUgbXVsdGlwbGUgaW5wdXRzIChhbmQgc3Vic2VxdWVudCBsYXllcnMpIGFsb25nIHdpdGggbXVsdGlwbGUKICAgICBvdXRwdXRzCiAgIC0gTmFtaW5nIG91ciBsYXllcnMgYWxsb3dzIHVzIHRvIGVhc2lseSB2aWV3IHRoZSBsYXllciBjb25uZWN0aW9ucwogICAtIEZvciBtb3JlIGluZm9ybWF0aW9uIG9uIGtlcmFzJyBmdW5jdGlvbmFsIG1vZGVsIHNlZToKICAgICAgLSBbRGVlcCBMZWFybmluZyB3aXRoIFJdKGh0dHBzOi8vYml0Lmx5LzJQdk9yQnYpLCBDaC4gNwogICAgICAtIFtHdWlkZSB0byB0aGUgRnVuY3Rpb25hbCBBUEldKGh0dHBzOi8vYml0Lmx5LzM1d1pxQXgpCgpb8J+PoF0oaHR0cHM6Ly9naXRodWIuY29tL21pc2stZGF0YS1zY2llbmNlL21pc2stZGwpCg==