Improve performance of package list expansion

Adds a hash table to the package list expansion to avoid iterating over
the children of package nodes that have already been visited. Without
this, the expansion is exponential. For library sets with a high degree
of dependency, iteration over the tree with revisiting results, in
practice, in significant slow down at best and pkg-config failure due to
memory exhaustion at worst. The resulting algorithm is equivalent to a
topological sort.
This commit is contained in:
Matthew Hanna 2016-07-26 15:28:17 -04:00 committed by Dan Nicholson
parent 87152c05be
commit 908cdd062a
16 changed files with 204 additions and 20 deletions

View file

@ -29,6 +29,7 @@ TESTS = \
check-relocatable \
check-variable-override \
check-variables \
check-dependencies \
$(NULL)
EXTRA_DIST = \
@ -102,4 +103,16 @@ EXTRA_DIST = \
pkgconfig/prefixdef-expanded.pc \
pcfiledir.pc \
variables.pc \
dependencies/a_dep_c.pc \
dependencies/b_dep_c.pc \
dependencies/c_dep.pc \
dependencies/d_dep_e_f.pc \
dependencies/d_dep_f_e.pc \
dependencies/e_dep_g_f.pc \
dependencies/f_dep_g.pc \
dependencies/g_dep.pc \
dependencies/h_dep_k_i_j.pc \
dependencies/i_dep_k_j.pc \
dependencies/j_dep_k.pc \
dependencies/k_dep.pc \
$(NULL)

44
check/check-dependencies Executable file
View file

@ -0,0 +1,44 @@
#! /bin/sh
# These tests check the evaluation of the 'recursive_fill_list' function to
# verify that for any package s that depends on t, that the library defined by
# package s occurs before that of package t
set -e
. ${srcdir}/common
export PKG_CONFIG_PATH
PKG_CONFIG_PATH="${srcdir}/dependencies"
# Shared dependency test.
RESULT="-la_dep_c -lb_dep_c -lc_dep"
run_test --libs a_dep_c b_dep_c
run_test --libs c_dep a_dep_c b_dep_c
run_test --libs a_dep_c c_dep b_dep_c
run_test --libs a_dep_c b_dep_c c_dep
# Redundancy test.
#
# Redundancy on the input line should not pass through.
RESULT="-la_dep_c -lb_dep_c -lc_dep"
run_test --libs a_dep_c a_dep_c b_dep_c
run_test --libs b_dep_c a_dep_c b_dep_c
# Diamond pattern test.
#
# One dependency of d depends on the other.
# Both dependencies of d depend on g
RESULT="-ld_dep_e_f -le_dep_g_f -lf_dep_g -lg_dep"
run_test --libs d_dep_e_f
RESULT="-ld_dep_f_e -le_dep_g_f -lf_dep_g -lg_dep"
run_test --libs d_dep_f_e
# Nested inclusion.
#
# Each package depends on all downsteam packages.
RESULT="-lh_dep_k_i_j -li_dep_k_j -lj_dep_k -lk_dep"
run_test --libs h_dep_k_i_j
run_test --libs h_dep_k_i_j i_dep_k_j
run_test --libs i_dep_k_j h_dep_k_i_j
run_test --libs k_dep j_dep_k i_dep_k_j h_dep_k_i_j

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -la_dep_c
Requires: c_dep

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -lb_dep_c
Requires: c_dep

View file

@ -0,0 +1,9 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -lc_dep

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -ld_dep_e_f
Requires: e_dep_g_f f_dep_g

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -ld_dep_f_e
Requires: f_dep_g e_dep_g_f

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -le_dep_g_f
Requires: f_dep_g g_dep

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -lf_dep_g
Requires: g_dep

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -lg_dep
Requires:

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -lh_dep_k_i_j
Requires: k_dep i_dep_k_j j_dep_k

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -li_dep_k_j
Requires: k_dep j_dep_k

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -lj_dep_k
Requires: k_dep

View file

@ -0,0 +1,10 @@
prefix=/path2
exec_prefix=${prefix}
libdir="${exec_prefix}/lib"
includedir="${prefix}/include"
Name: Dependencies test.
Description: Test package for testing dependency order.
Version: 1.0.0
Libs: -lk_dep
Requires:

47
pkg.c
View file

@ -525,35 +525,44 @@ packages_sort_by_path_position (GList *list)
return g_list_sort (list, pathposcmp);
}
/* Construct a topological sort of all required packages.
*
* This is a depth first search starting from the right. The output 'listp' is
* in reverse order, with the first node reached in the depth first search at
* the end of the list. Previously visited nodes are skipped. The result is
* a list of packages such that each packages is listed once and comes before
* any package that it depends on.
*/
static void
recursive_fill_list (Package *pkg, gboolean include_private, GList **listp)
recursive_fill_list (Package *pkg, gboolean include_private,
GHashTable *visited, GList **listp)
{
GList *tmp;
/*
* If the package is one of the parents, we can skip it. This allows
* circular requires loops to be broken.
* If the package has already been visited, then it is already in 'listp' and
* we can skip it. Additionally, this allows circular requires loops to be
* broken.
*/
if (pkg->in_requires_chain)
if (g_hash_table_lookup_extended (visited, pkg->key, NULL, NULL))
{
debug_spew ("Package %s already in requires chain, skipping\n",
pkg->key);
return;
}
/* record this package in the dependency chain */
pkg->in_requires_chain = TRUE;
else
{
g_hash_table_replace (visited, pkg->key, pkg->key);
}
/* Start from the end of the required package list to maintain order since
* the recursive list is built by prepending. */
tmp = include_private ? pkg->requires_private : pkg->requires;
for (tmp = g_list_last (tmp); tmp != NULL; tmp = g_list_previous (tmp))
recursive_fill_list (tmp->data, include_private, listp);
recursive_fill_list (tmp->data, include_private, visited, listp);
*listp = g_list_prepend (*listp, pkg);
/* remove this package from the dependency chain now that we've unwound */
pkg->in_requires_chain = FALSE;
}
/* merge the flags from the individual packages */
@ -634,18 +643,15 @@ fill_list (GList *packages, FlagType type,
GList *tmp;
GList *expanded = NULL;
GList *flags;
GHashTable *visited;
/* Start from the end of the requested package list to maintain order since
* the recursive list is built by prepending. */
visited = g_hash_table_new (g_str_hash, g_str_equal);
for (tmp = g_list_last (packages); tmp != NULL; tmp = g_list_previous (tmp))
recursive_fill_list (tmp->data, include_private, &expanded);
/* Remove duplicate packages from the recursive list. This should provide a
* serialized package list where all interdependencies are resolved
* consistently. */
spew_package_list (" pre-remove", expanded);
expanded = package_list_strip_duplicates (expanded);
spew_package_list ("post-remove", expanded);
recursive_fill_list (tmp->data, include_private, visited, &expanded);
g_hash_table_destroy (visited);
spew_package_list ("post-recurse", expanded);
if (in_path_order)
{
@ -686,6 +692,7 @@ verify_package (Package *pkg)
GList *requires_iter;
GList *conflicts_iter;
GList *system_dir_iter = NULL;
GHashTable *visited;
int count;
const gchar *search_path;
@ -756,7 +763,9 @@ verify_package (Package *pkg)
/* Make sure we didn't drag in any conflicts via Requires
* (inefficient algorithm, who cares)
*/
recursive_fill_list (pkg, TRUE, &requires);
visited = g_hash_table_new (g_str_hash, g_str_equal);
recursive_fill_list (pkg, TRUE, visited, &requires);
g_hash_table_destroy (visited);
conflicts = pkg->conflicts;
requires_iter = requires;

1
pkg.h
View file

@ -84,7 +84,6 @@ struct Package_
int path_position; /* used to order packages by position in path of their .pc file, lower number means earlier in path */
int libs_num; /* Number of times the "Libs" header has been seen */
int libs_private_num; /* Number of times the "Libs.private" header has been seen */
gboolean in_requires_chain; /* package is in current Requires chain */
char *orig_prefix; /* original prefix value before redefinition */
};