├── .bazelrc ├── .bazelversion ├── .bcr ├── config.yml ├── metadata.template.json ├── presubmit.yml └── source.template.json ├── .github └── workflows │ ├── publish-rabbitmq-bcr.yml │ ├── release.yml │ ├── test.yml │ ├── update-otp-patch-version.yaml │ └── update-otp-patches.yaml ├── .gitignore ├── BUILD.bazel ├── LICENSE ├── LICENSE-APACHE2 ├── LICENSE-MPL-RabbitMQ ├── MODULE.bazel ├── Makefile ├── README.asciidoc ├── WORKSPACE.bazel ├── c_src └── lg_tracer.c ├── doc └── src │ └── guide │ ├── book.asciidoc │ ├── callgrind.asciidoc │ ├── flame.asciidoc │ ├── introduction.asciidoc │ ├── messages.asciidoc │ ├── seq.png │ └── tracing.asciidoc ├── ebin └── looking_glass.app ├── erlang.mk ├── extensions.bzl ├── platforms └── BUILD.bazel ├── rebar.config ├── src ├── lg.erl ├── lg_callgrind.erl ├── lg_file_reader.erl ├── lg_file_tracer.erl ├── lg_flame.erl ├── lg_messages.erl ├── lg_messages_seqdiag.erl ├── lg_rabbit_hole.erl ├── lg_raw_console_tracer.erl ├── lg_socket_client.erl ├── lg_socket_tracer.erl ├── lg_term.erl ├── lg_tracer.erl ├── lg_tracer_pool.erl ├── looking_glass_app.erl └── looking_glass_sup.erl ├── test └── lg_SUITE.erl ├── user-template.bazelrc └── util.bzl /.bazelrc: -------------------------------------------------------------------------------- 1 | build --registry=https://bcr.bazel.build/ 2 | build --registry=https://raw.githubusercontent.com/rabbitmq/bazel-central-registry/erlang-packages/ 3 | 4 | build --incompatible_strict_action_env 5 | build --local_test_jobs=1 6 | 7 | build --flag_alias=erlang_home=@rules_erlang//:erlang_home 8 | build --flag_alias=erlang_version=@rules_erlang//:erlang_version 9 | 10 | build --extra_toolchains="@erlang_config//..." 11 | 12 | build:buildbuddy --bes_results_url=https://app.buildbuddy.io/invocation/ 13 | build:buildbuddy --bes_backend=grpcs://remote.buildbuddy.io 14 | build:buildbuddy --remote_cache=grpcs://remote.buildbuddy.io 15 | build:buildbuddy --remote_timeout=1200 16 | build:buildbuddy --grpc_keepalive_time=30s 17 | build:buildbuddy --build_metadata=REPO_URL=https://github.com/rabbitmq/looking_glass.git 18 | build:buildbuddy --experimental_remote_cache_compression 19 | build:buildbuddy --experimental_remote_cache_async 20 | build:buildbuddy --noslim_profile 21 | build:buildbuddy --experimental_profile_include_target_label 22 | build:buildbuddy --experimental_profile_include_primary_output 23 | 24 | build:rbe --config=buildbuddy 25 | 26 | build:rbe --remote_executor=grpcs://remote.buildbuddy.io 27 | 28 | build:rbe --spawn_strategy=remote 29 | build:rbe --test_strategy="" 30 | build:rbe --jobs=50 31 | 32 | build:rbe --crosstool_top=@rbe//cc:toolchain 33 | build:rbe --extra_toolchains=@rbe//config:cc-toolchain 34 | 35 | build:rbe --host_platform=//platforms:erlang_internal_platform 36 | 37 | build:rbe --host_cpu=k8 38 | build:rbe --cpu=k8 39 | 40 | build:rbe-24 --config=rbe 41 | build:rbe-24 --platforms=//platforms:erlang_linux_24_platform 42 | 43 | build:rbe-25 --config=rbe 44 | build:rbe-25 --platforms=//platforms:erlang_linux_25_platform 45 | 46 | build:rbe-26 --config=rbe 47 | build:rbe-26 --platforms=//platforms:erlang_linux_26_platform 48 | 49 | # Try importing a user specific .bazelrc 50 | # You can create your own by copying and editing the template-user.bazelrc template: 51 | # cp template-user.bazelrc user.bazelrc 52 | try-import %workspace%/user.bazelrc 53 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | latest 2 | -------------------------------------------------------------------------------- /.bcr/config.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabbitmq/looking_glass/942c9e1a32d75b22e831091b27829f54f0e57569/.bcr/config.yml -------------------------------------------------------------------------------- /.bcr/metadata.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://github.com/rabbitmq/looking_glass", 3 | "maintainers": [ 4 | { 5 | "email": "kuryloskip@vmware.com", 6 | "github": "pjk25", 7 | "name": "Rin Kuryloski" 8 | } 9 | ], 10 | "repository": [ 11 | "github:rabbitmq/looking_glass" 12 | ], 13 | "versions": [], 14 | "yanked_versions": {} 15 | } -------------------------------------------------------------------------------- /.bcr/presubmit.yml: -------------------------------------------------------------------------------- 1 | shell_commands: &shell_commands 2 | - curl -O https://raw.githubusercontent.com/kerl/kerl/master/kerl 3 | - chmod a+x kerl 4 | - ./kerl update releases 5 | - ./kerl build ${ERLANG_VERSION} 6 | - ./kerl install ${ERLANG_VERSION} ~/kerl/${ERLANG_VERSION} 7 | - realpath ~/kerl/${ERLANG_VERSION} 8 | 9 | platforms: 10 | macos: 11 | environment: 12 | ERLANG_VERSION: "25.0" 13 | ERLANG_HOME: /Users/buildkite/kerl/25.0 14 | shell_commands: *shell_commands 15 | build_flags: 16 | - --incompatible_strict_action_env 17 | build_targets: 18 | - '@com_github_rabbitmq_looking_glass//:erlang_app' 19 | ubuntu2004: 20 | environment: 21 | ERLANG_VERSION: "25.0" 22 | ERLANG_HOME: /var/lib/buildkite-agent/kerl/25.0 23 | shell_commands: *shell_commands 24 | build_flags: 25 | - --incompatible_strict_action_env 26 | build_targets: 27 | - '@com_github_rabbitmq_looking_glass//:erlang_app' 28 | -------------------------------------------------------------------------------- /.bcr/source.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrity": "", 3 | "strip_prefix": "{REPO}-{VERSION}", 4 | "url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/looking_glass-{VERSION}.tar.gz" 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/publish-rabbitmq-bcr.yml: -------------------------------------------------------------------------------- 1 | name: Add to rabbitmq/bazel-central-registry@erlang-packages 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | add-module: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: CHECKOUT 11 | uses: actions/checkout@v4 12 | with: 13 | path: looking_glass 14 | - name: CHECKOUT rabbitmq/bazel-central-registry@erlang-packages 15 | uses: actions/checkout@v4 16 | with: 17 | repository: rabbitmq/bazel-central-registry 18 | path: bazel-central-registry 19 | ref: erlang-packages 20 | - name: PUBLISH TO rabbitmq/bazel-central-registry@erlang-packages 21 | working-directory: bazel-central-registry 22 | env: 23 | MODULE_NAME: com_github_rabbitmq_looking_glass 24 | REPO_NAME: looking_glass 25 | run: | 26 | VERSION="${{ github.ref_name }}" 27 | MAJOR="${VERSION:0:1}" 28 | 29 | echo "VERSION: ${VERSION}" 30 | echo "MAJOR: ${MAJOR}" 31 | 32 | cat << EOF > ${MODULE_NAME}.json 33 | { 34 | "build_file": null, 35 | "build_targets": [ 36 | "@${MODULE_NAME}//:${MODULE_NAME}", 37 | "@${MODULE_NAME}//:erlang_app" 38 | ], 39 | "compatibility_level": "$((${MAJOR} + 1))", 40 | "deps": [], 41 | "module_dot_bazel": "${{ github.workspace }}/${REPO_NAME}/MODULE.bazel", 42 | "name": "${MODULE_NAME}", 43 | "patch_strip": 0, 44 | "patches": [], 45 | "presubmit_yml": "${{ github.workspace }}/${REPO_NAME}/.bcr/presubmit.yml", 46 | "strip_prefix": "${REPO_NAME}-${VERSION}", 47 | "test_module_build_targets": [], 48 | "test_module_path": null, 49 | "test_module_test_targets": [], 50 | "url": "https://github.com/${{ github.repository }}/releases/download/${VERSION}/${REPO_NAME}-${VERSION}.tar.gz", 51 | "version": "${VERSION}" 52 | } 53 | EOF 54 | 55 | jq '.' ${MODULE_NAME}.json 56 | 57 | pip install -r tools/requirements_lock.txt 58 | python3 tools/add_module.py \ 59 | --input=${MODULE_NAME}.json 60 | 61 | git diff 62 | - name: CREATE PULL REQUEST 63 | uses: peter-evans/create-pull-request@v6.0.3 64 | with: 65 | token: ${{ secrets.REPO_SCOPED_TOKEN }} 66 | path: bazel-central-registry 67 | title: Add ${{ github.repository }}@${{ github.ref_name }} 68 | body: > 69 | Automated changes created by 70 | ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 71 | using the [create-pull-request](https://github.com/peter-evans/create-pull-request) 72 | GitHub action in the ${{ github.workflow }} workflow. 73 | commit-message: | 74 | Add ${{ github.repository }}@${{ github.ref_name }} 75 | branch: add-${{ github.repository }}@${{ github.ref_name }} 76 | delete-branch: true 77 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 0.* 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: CHECKOUT 11 | uses: actions/checkout@v3 12 | with: 13 | path: looking_glass 14 | - name: ASSERT VERSIONS 15 | id: versions 16 | working-directory: looking_glass 17 | run: | 18 | sudo npm install --global --silent @bazel/buildozer 19 | 20 | VERSION_ERLANG_MK="$(sed -ne 's/PROJECT_VERSION = \(.*\)/\1/p' Makefile)" 21 | 22 | if [[ "${{ github.ref_name }}" != "$VERSION_ERLANG_MK" ]]; then 23 | echo "Version in Makefile ($VERSION_ERLANG_MK) does not match tag (${{ github.ref_name }})" 24 | exit 1 25 | fi 26 | 27 | VERSION_BAZEL="$(cat MODULE.bazel | buildozer 'print version' -:%module)" 28 | 29 | if [[ "${{ github.ref_name }}" != "$VERSION_BAZEL" ]]; then 30 | echo "Version in MODULE.bazel ($VERSION_BAZEL) does not match tag (${{ github.ref_name }})" 31 | exit 1 32 | fi 33 | 34 | echo "version=$VERSION_BAZEL" | tee -a $GITHUB_OUTPUT 35 | - name: FETCH THE SOURCE ARCHIVE 36 | run: | 37 | curl \ 38 | -L \ 39 | -o looking_glass-${{ steps.versions.outputs.version }}.tar.gz \ 40 | https://github.com/${{ github.repository }}/archive/${{ github.ref }}.tar.gz 41 | - name: CREATE RELEASE 42 | id: create-release 43 | uses: ncipollo/release-action@v1.12.0 44 | with: 45 | token: ${{ secrets.REPO_SCOPED_TOKEN }} 46 | allowUpdates: true 47 | artifactErrorsFailBuild: true 48 | updateOnlyUnreleased: true 49 | generateReleaseNotes: true 50 | artifacts: >- 51 | looking_glass-${{ steps.versions.outputs.version }}.tar.gz 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | bazel_otp_name: 14 | - "25" 15 | - "26" 16 | steps: 17 | - name: CHECKOUT 18 | uses: actions/checkout@v3 19 | - name: MOUNT BAZEL CACHE 20 | uses: actions/cache@v3.3.1 21 | with: 22 | path: "/home/runner/repo-cache/" 23 | key: ${{ runner.os }}-repo-cache-${{ hashFiles('MODULE.bazel','WORKSPACE.bazel') }} 24 | restore-keys: | 25 | ${{ runner.os }}-repo-cache- 26 | - name: CONFIGURE BAZEL 27 | run: | 28 | if [ -n "${{ secrets.BUILDBUDDY_API_KEY }}" ]; then 29 | cat << EOF >> user.bazelrc 30 | build:buildbuddy --remote_header=x-buildbuddy-api-key=${{ secrets.BUILDBUDDY_API_KEY }} 31 | EOF 32 | fi 33 | cat << EOF >> user.bazelrc 34 | build:buildbuddy --build_metadata=ROLE=CI 35 | build:buildbuddy --build_metadata=VISIBILITY=PUBLIC 36 | build:buildbuddy --repository_cache=/home/runner/repo-cache/ 37 | build:buildbuddy --color=yes 38 | build:buildbuddy --disk_cache= 39 | EOF 40 | 41 | bazelisk info release 42 | - name: TEST 43 | run: | 44 | bazelisk test //... \ 45 | --config=rbe-${{ matrix.bazel_otp_name }} \ 46 | --verbose_failures 47 | -------------------------------------------------------------------------------- /.github/workflows/update-otp-patch-version.yaml: -------------------------------------------------------------------------------- 1 | name: Update OTP Patch Version 2 | on: 3 | workflow_call: 4 | inputs: 5 | target_name: 6 | required: true 7 | type: string 8 | erlang_version: 9 | required: true 10 | type: string 11 | secrets: 12 | REPO_SCOPED_TOKEN: 13 | required: true 14 | jobs: 15 | update-toolchains: 16 | name: Update OTP ${{ inputs.erlang_version }} Patch Version 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | steps: 20 | - name: CHECKOUT REPOSITORY 21 | uses: actions/checkout@v3 22 | - name: DETERMINE LATEST PATCH & SHA 23 | id: fetch-version 24 | run: | 25 | TAG_NAME=$(curl -s GET https://api.github.com/repos/erlang/otp/tags?per_page=100 \ 26 | | jq -r 'map(select(.name | contains("OTP-${{ inputs.erlang_version }}"))) | first | .name') 27 | VERSION=${TAG_NAME#OTP-} 28 | if [[ -z "${VERSION}" ]]; then 29 | echo "Failed to determine latest VERSION for OTP-${{ inputs.erlang_version }}" 30 | exit 1 31 | fi 32 | ARCHIVE_RBE_URL="https://github.com/erlang/otp/releases/download/${TAG_NAME}/otp_src_${VERSION}.tar.gz" 33 | wget --continue --quiet --output-document="/tmp/otp_src_${VERSION}.tar.gz" "${ARCHIVE_RBE_URL}" 34 | SHA="$(shasum -a 256 "/tmp/otp_src_${VERSION}.tar.gz" | awk '{print $1}')" 35 | if [[ -z "${SHA}" ]]; then 36 | echo "Failed to determine SHA for ${TAG_NAME}" 37 | exit 1 38 | fi 39 | echo "::set-output name=VERSION::${VERSION}" 40 | echo "::set-output name=SHA::${SHA}" 41 | - name: MODIFY VERSION FILE 42 | run: | 43 | sudo npm install --global --silent @bazel/buildozer 44 | echo "$(cat MODULE.bazel | buildozer 'set sha256 "${{ steps.fetch-version.outputs.SHA }}"' -:${{ inputs.target_name }})" > MODULE.bazel 45 | echo "$(cat MODULE.bazel | buildozer 'set version "${{ steps.fetch-version.outputs.VERSION }}"' -:${{ inputs.target_name }})" > MODULE.bazel 46 | set -x 47 | git diff 48 | - name: CREATE PULL REQUEST 49 | id: cpr 50 | uses: peter-evans/create-pull-request@v5.0.1 51 | with: 52 | token: ${{ secrets.REPO_SCOPED_TOKEN }} 53 | committer: GitHub 54 | author: GitHub 55 | title: Adopt otp ${{ steps.fetch-version.outputs.VERSION }} 56 | body: > 57 | Automated changes created by 58 | ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 59 | using the [create-pull-request](https://github.com/peter-evans/create-pull-request) 60 | GitHub action in the ${{ github.workflow }} workflow. 61 | commit-message: | 62 | Adopt otp ${{ steps.fetch-version.outputs.VERSION }} 63 | branch: bump-otp-${{ inputs.erlang_version }} 64 | delete-branch: true 65 | - name: ENABLE PULL REQUEST AUTOMERGE 66 | if: steps.cpr.outputs.pull-request-operation == 'created' 67 | uses: peter-evans/enable-pull-request-automerge@v3 68 | with: 69 | token: ${{ secrets.REPO_SCOPED_TOKEN }} 70 | pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} 71 | merge-method: squash 72 | -------------------------------------------------------------------------------- /.github/workflows/update-otp-patches.yaml: -------------------------------------------------------------------------------- 1 | name: Update OTP Patch Versions 2 | on: 3 | schedule: 4 | - cron: '0 3 * * *' 5 | workflow_dispatch: 6 | jobs: 7 | update-toolchains: 8 | uses: ./.github/workflows/update-otp-patch-version.yaml 9 | with: 10 | target_name: ${{ matrix.name }} 11 | erlang_version: ${{ matrix.erlang_version }} 12 | secrets: 13 | REPO_SCOPED_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }} 14 | strategy: 15 | max-parallel: 1 16 | fail-fast: false 17 | matrix: 18 | erlang_version: 19 | - "24.3" 20 | - "25.3" 21 | - "26.0" 22 | include: 23 | - erlang_version: "24.3" 24 | name: '24' 25 | - erlang_version: "25.3" 26 | name: '25' 27 | - erlang_version: "26.0" 28 | name: '26' 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .erlang.mk/ 2 | .looking_glass.plt 3 | c_src/env.mk 4 | c_src/*.o 5 | deps/ 6 | ebin/*.beam 7 | ebin/test 8 | logs/ 9 | looking_glass.d 10 | priv/lg_tracer.so 11 | test/*.beam 12 | 13 | /user.bazelrc 14 | /bazel-* 15 | 16 | /.vscode/ 17 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_erlang//:erlang_app.bzl", 3 | "DEFAULT_TEST_ERLC_OPTS", 4 | "erlang_app", 5 | "test_erlang_app", 6 | ) 7 | load("@rules_erlang//:xref2.bzl", "xref") 8 | load("@rules_erlang//:dialyze.bzl", "DEFAULT_PLT_APPS", "dialyze", "plt") 9 | load("@rules_erlang//:eunit2.bzl", "eunit") 10 | load("@rules_erlang//:ct.bzl", "assert_suites2", "ct_suite") 11 | load(":util.bzl", "common_root_as_var") 12 | 13 | APP_NAME = "looking_glass" 14 | 15 | APP_DESCRIPTION = "New project" 16 | 17 | APP_VERSION = module_version() 18 | 19 | EXTRA_APPS = [ 20 | "runtime_tools", 21 | ] 22 | 23 | RUNTIME_DEPS = [ 24 | "@lz4-erlang//:erlang_app", 25 | ] 26 | 27 | TEST_ERLC_OPTS = DEFAULT_TEST_ERLC_OPTS + [ 28 | "+{parse_transform, eunit_autoexport}", 29 | ] 30 | 31 | common_root_as_var( 32 | name = "erlang_headers_dir", 33 | srcs = [ 34 | "@rules_erlang//tools:erlang_headers", 35 | ], 36 | ) 37 | 38 | common_root_as_var( 39 | name = "nif_helpers_dir", 40 | srcs = [ 41 | "@nif_helpers//:nif_helpers.h", 42 | ], 43 | ) 44 | 45 | cc_binary( 46 | name = "lg_tracer", 47 | srcs = glob([ 48 | "c_src/**/*.c", 49 | "c_src/**/*.h", 50 | ]) + [ 51 | "@rules_erlang//tools:erlang_headers", 52 | "@nif_helpers//:nif_helpers.h", 53 | "@nif_helpers//:nif_helpers.c", 54 | ], 55 | copts = [ 56 | "-I $(ERLANG_HEADERS_DIR)", 57 | "-I $(NIF_HELPERS_DIR)", 58 | "-fPIC", 59 | ] + select({ 60 | "@platforms//os:macos": ["-Wno-implicit-function-declaration"], 61 | "//conditions:default": [], 62 | }), 63 | linkopts = select({ 64 | "@platforms//os:macos": ["-Wl,-undefined,dynamic_lookup"], 65 | "//conditions:default": [], 66 | }), 67 | linkshared = True, 68 | toolchains = [ 69 | ":erlang_headers_dir", 70 | ":nif_helpers_dir", 71 | ], 72 | ) 73 | 74 | genrule( 75 | name = "lg_tracer_so", 76 | srcs = [":lg_tracer"], 77 | outs = ["priv/lg_tracer.so"], 78 | cmd = "cp $< $@", 79 | ) 80 | 81 | erlang_app( 82 | app_description = APP_DESCRIPTION, 83 | app_name = APP_NAME, 84 | app_version = APP_VERSION, 85 | extra_apps = EXTRA_APPS, 86 | extra_priv = ["priv/lg_tracer.so"], 87 | runtime_deps = RUNTIME_DEPS, 88 | ) 89 | 90 | test_erlang_app( 91 | app_description = APP_DESCRIPTION, 92 | app_name = APP_NAME, 93 | app_version = APP_VERSION, 94 | erlc_opts = TEST_ERLC_OPTS, 95 | extra_apps = EXTRA_APPS, 96 | extra_priv = ["priv/lg_tracer.so"], 97 | runtime_deps = RUNTIME_DEPS, 98 | ) 99 | 100 | xref() 101 | 102 | plt( 103 | name = "deps_plt", 104 | apps = DEFAULT_PLT_APPS, 105 | for_target = ":erlang_app", 106 | ) 107 | 108 | dialyze( 109 | size = "small", 110 | plt = ":deps_plt", 111 | ) 112 | 113 | eunit( 114 | name = "eunit", 115 | target = ":test_erlang_app", 116 | ) 117 | 118 | ct_suite( 119 | name = "lg_SUITE", 120 | runtime_deps = [ 121 | "@ct_helper//:erlang_app", 122 | ], 123 | ) 124 | 125 | assert_suites2() 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This package, Looking Glass, is double-licensed under the Mozilla 2 | Public License 1.1 ("MPL") and the Apache License version 2 3 | ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 4 | please see LICENSE-APACHE2. 5 | 6 | If you have any questions regarding licensing, please contact us at 7 | info@rabbitmq.com. 8 | -------------------------------------------------------------------------------- /LICENSE-APACHE2: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Pivotal Software, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | https://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MPL-RabbitMQ: -------------------------------------------------------------------------------- 1 | MOZILLA PUBLIC LICENSE 2 | Version 1.1 3 | 4 | --------------- 5 | 6 | 1. Definitions. 7 | 8 | 1.0.1. "Commercial Use" means distribution or otherwise making the 9 | Covered Code available to a third party. 10 | 11 | 1.1. "Contributor" means each entity that creates or contributes to 12 | the creation of Modifications. 13 | 14 | 1.2. "Contributor Version" means the combination of the Original 15 | Code, prior Modifications used by a Contributor, and the Modifications 16 | made by that particular Contributor. 17 | 18 | 1.3. "Covered Code" means the Original Code or Modifications or the 19 | combination of the Original Code and Modifications, in each case 20 | including portions thereof. 21 | 22 | 1.4. "Electronic Distribution Mechanism" means a mechanism generally 23 | accepted in the software development community for the electronic 24 | transfer of data. 25 | 26 | 1.5. "Executable" means Covered Code in any form other than Source 27 | Code. 28 | 29 | 1.6. "Initial Developer" means the individual or entity identified 30 | as the Initial Developer in the Source Code notice required by Exhibit 31 | A. 32 | 33 | 1.7. "Larger Work" means a work which combines Covered Code or 34 | portions thereof with code not governed by the terms of this License. 35 | 36 | 1.8. "License" means this document. 37 | 38 | 1.8.1. "Licensable" means having the right to grant, to the maximum 39 | extent possible, whether at the time of the initial grant or 40 | subsequently acquired, any and all of the rights conveyed herein. 41 | 42 | 1.9. "Modifications" means any addition to or deletion from the 43 | substance or structure of either the Original Code or any previous 44 | Modifications. When Covered Code is released as a series of files, a 45 | Modification is: 46 | A. Any addition to or deletion from the contents of a file 47 | containing Original Code or previous Modifications. 48 | 49 | B. Any new file that contains any part of the Original Code or 50 | previous Modifications. 51 | 52 | 1.10. "Original Code" means Source Code of computer software code 53 | which is described in the Source Code notice required by Exhibit A as 54 | Original Code, and which, at the time of its release under this 55 | License is not already Covered Code governed by this License. 56 | 57 | 1.10.1. "Patent Claims" means any patent claim(s), now owned or 58 | hereafter acquired, including without limitation, method, process, 59 | and apparatus claims, in any patent Licensable by grantor. 60 | 61 | 1.11. "Source Code" means the preferred form of the Covered Code for 62 | making modifications to it, including all modules it contains, plus 63 | any associated interface definition files, scripts used to control 64 | compilation and installation of an Executable, or source code 65 | differential comparisons against either the Original Code or another 66 | well known, available Covered Code of the Contributor's choice. The 67 | Source Code can be in a compressed or archival form, provided the 68 | appropriate decompression or de-archiving software is widely available 69 | for no charge. 70 | 71 | 1.12. "You" (or "Your") means an individual or a legal entity 72 | exercising rights under, and complying with all of the terms of, this 73 | License or a future version of this License issued under Section 6.1. 74 | For legal entities, "You" includes any entity which controls, is 75 | controlled by, or is under common control with You. For purposes of 76 | this definition, "control" means (a) the power, direct or indirect, 77 | to cause the direction or management of such entity, whether by 78 | contract or otherwise, or (b) ownership of more than fifty percent 79 | (50%) of the outstanding shares or beneficial ownership of such 80 | entity. 81 | 82 | 2. Source Code License. 83 | 84 | 2.1. The Initial Developer Grant. 85 | The Initial Developer hereby grants You a world-wide, royalty-free, 86 | non-exclusive license, subject to third party intellectual property 87 | claims: 88 | (a) under intellectual property rights (other than patent or 89 | trademark) Licensable by Initial Developer to use, reproduce, 90 | modify, display, perform, sublicense and distribute the Original 91 | Code (or portions thereof) with or without Modifications, and/or 92 | as part of a Larger Work; and 93 | 94 | (b) under Patents Claims infringed by the making, using or 95 | selling of Original Code, to make, have made, use, practice, 96 | sell, and offer for sale, and/or otherwise dispose of the 97 | Original Code (or portions thereof). 98 | 99 | (c) the licenses granted in this Section 2.1(a) and (b) are 100 | effective on the date Initial Developer first distributes 101 | Original Code under the terms of this License. 102 | 103 | (d) Notwithstanding Section 2.1(b) above, no patent license is 104 | granted: 1) for code that You delete from the Original Code; 2) 105 | separate from the Original Code; or 3) for infringements caused 106 | by: i) the modification of the Original Code or ii) the 107 | combination of the Original Code with other software or devices. 108 | 109 | 2.2. Contributor Grant. 110 | Subject to third party intellectual property claims, each Contributor 111 | hereby grants You a world-wide, royalty-free, non-exclusive license 112 | 113 | (a) under intellectual property rights (other than patent or 114 | trademark) Licensable by Contributor, to use, reproduce, modify, 115 | display, perform, sublicense and distribute the Modifications 116 | created by such Contributor (or portions thereof) either on an 117 | unmodified basis, with other Modifications, as Covered Code 118 | and/or as part of a Larger Work; and 119 | 120 | (b) under Patent Claims infringed by the making, using, or 121 | selling of Modifications made by that Contributor either alone 122 | and/or in combination with its Contributor Version (or portions 123 | of such combination), to make, use, sell, offer for sale, have 124 | made, and/or otherwise dispose of: 1) Modifications made by that 125 | Contributor (or portions thereof); and 2) the combination of 126 | Modifications made by that Contributor with its Contributor 127 | Version (or portions of such combination). 128 | 129 | (c) the licenses granted in Sections 2.2(a) and 2.2(b) are 130 | effective on the date Contributor first makes Commercial Use of 131 | the Covered Code. 132 | 133 | (d) Notwithstanding Section 2.2(b) above, no patent license is 134 | granted: 1) for any code that Contributor has deleted from the 135 | Contributor Version; 2) separate from the Contributor Version; 136 | 3) for infringements caused by: i) third party modifications of 137 | Contributor Version or ii) the combination of Modifications made 138 | by that Contributor with other software (except as part of the 139 | Contributor Version) or other devices; or 4) under Patent Claims 140 | infringed by Covered Code in the absence of Modifications made by 141 | that Contributor. 142 | 143 | 3. Distribution Obligations. 144 | 145 | 3.1. Application of License. 146 | The Modifications which You create or to which You contribute are 147 | governed by the terms of this License, including without limitation 148 | Section 2.2. The Source Code version of Covered Code may be 149 | distributed only under the terms of this License or a future version 150 | of this License released under Section 6.1, and You must include a 151 | copy of this License with every copy of the Source Code You 152 | distribute. You may not offer or impose any terms on any Source Code 153 | version that alters or restricts the applicable version of this 154 | License or the recipients' rights hereunder. However, You may include 155 | an additional document offering the additional rights described in 156 | Section 3.5. 157 | 158 | 3.2. Availability of Source Code. 159 | Any Modification which You create or to which You contribute must be 160 | made available in Source Code form under the terms of this License 161 | either on the same media as an Executable version or via an accepted 162 | Electronic Distribution Mechanism to anyone to whom you made an 163 | Executable version available; and if made available via Electronic 164 | Distribution Mechanism, must remain available for at least twelve (12) 165 | months after the date it initially became available, or at least six 166 | (6) months after a subsequent version of that particular Modification 167 | has been made available to such recipients. You are responsible for 168 | ensuring that the Source Code version remains available even if the 169 | Electronic Distribution Mechanism is maintained by a third party. 170 | 171 | 3.3. Description of Modifications. 172 | You must cause all Covered Code to which You contribute to contain a 173 | file documenting the changes You made to create that Covered Code and 174 | the date of any change. You must include a prominent statement that 175 | the Modification is derived, directly or indirectly, from Original 176 | Code provided by the Initial Developer and including the name of the 177 | Initial Developer in (a) the Source Code, and (b) in any notice in an 178 | Executable version or related documentation in which You describe the 179 | origin or ownership of the Covered Code. 180 | 181 | 3.4. Intellectual Property Matters 182 | (a) Third Party Claims. 183 | If Contributor has knowledge that a license under a third party's 184 | intellectual property rights is required to exercise the rights 185 | granted by such Contributor under Sections 2.1 or 2.2, 186 | Contributor must include a text file with the Source Code 187 | distribution titled "LEGAL" which describes the claim and the 188 | party making the claim in sufficient detail that a recipient will 189 | know whom to contact. If Contributor obtains such knowledge after 190 | the Modification is made available as described in Section 3.2, 191 | Contributor shall promptly modify the LEGAL file in all copies 192 | Contributor makes available thereafter and shall take other steps 193 | (such as notifying appropriate mailing lists or newsgroups) 194 | reasonably calculated to inform those who received the Covered 195 | Code that new knowledge has been obtained. 196 | 197 | (b) Contributor APIs. 198 | If Contributor's Modifications include an application programming 199 | interface and Contributor has knowledge of patent licenses which 200 | are reasonably necessary to implement that API, Contributor must 201 | also include this information in the LEGAL file. 202 | 203 | (c) Representations. 204 | Contributor represents that, except as disclosed pursuant to 205 | Section 3.4(a) above, Contributor believes that Contributor's 206 | Modifications are Contributor's original creation(s) and/or 207 | Contributor has sufficient rights to grant the rights conveyed by 208 | this License. 209 | 210 | 3.5. Required Notices. 211 | You must duplicate the notice in Exhibit A in each file of the Source 212 | Code. If it is not possible to put such notice in a particular Source 213 | Code file due to its structure, then You must include such notice in a 214 | location (such as a relevant directory) where a user would be likely 215 | to look for such a notice. If You created one or more Modification(s) 216 | You may add your name as a Contributor to the notice described in 217 | Exhibit A. You must also duplicate this License in any documentation 218 | for the Source Code where You describe recipients' rights or ownership 219 | rights relating to Covered Code. You may choose to offer, and to 220 | charge a fee for, warranty, support, indemnity or liability 221 | obligations to one or more recipients of Covered Code. However, You 222 | may do so only on Your own behalf, and not on behalf of the Initial 223 | Developer or any Contributor. You must make it absolutely clear than 224 | any such warranty, support, indemnity or liability obligation is 225 | offered by You alone, and You hereby agree to indemnify the Initial 226 | Developer and every Contributor for any liability incurred by the 227 | Initial Developer or such Contributor as a result of warranty, 228 | support, indemnity or liability terms You offer. 229 | 230 | 3.6. Distribution of Executable Versions. 231 | You may distribute Covered Code in Executable form only if the 232 | requirements of Section 3.1-3.5 have been met for that Covered Code, 233 | and if You include a notice stating that the Source Code version of 234 | the Covered Code is available under the terms of this License, 235 | including a description of how and where You have fulfilled the 236 | obligations of Section 3.2. The notice must be conspicuously included 237 | in any notice in an Executable version, related documentation or 238 | collateral in which You describe recipients' rights relating to the 239 | Covered Code. You may distribute the Executable version of Covered 240 | Code or ownership rights under a license of Your choice, which may 241 | contain terms different from this License, provided that You are in 242 | compliance with the terms of this License and that the license for the 243 | Executable version does not attempt to limit or alter the recipient's 244 | rights in the Source Code version from the rights set forth in this 245 | License. If You distribute the Executable version under a different 246 | license You must make it absolutely clear that any terms which differ 247 | from this License are offered by You alone, not by the Initial 248 | Developer or any Contributor. You hereby agree to indemnify the 249 | Initial Developer and every Contributor for any liability incurred by 250 | the Initial Developer or such Contributor as a result of any such 251 | terms You offer. 252 | 253 | 3.7. Larger Works. 254 | You may create a Larger Work by combining Covered Code with other code 255 | not governed by the terms of this License and distribute the Larger 256 | Work as a single product. In such a case, You must make sure the 257 | requirements of this License are fulfilled for the Covered Code. 258 | 259 | 4. Inability to Comply Due to Statute or Regulation. 260 | 261 | If it is impossible for You to comply with any of the terms of this 262 | License with respect to some or all of the Covered Code due to 263 | statute, judicial order, or regulation then You must: (a) comply with 264 | the terms of this License to the maximum extent possible; and (b) 265 | describe the limitations and the code they affect. Such description 266 | must be included in the LEGAL file described in Section 3.4 and must 267 | be included with all distributions of the Source Code. Except to the 268 | extent prohibited by statute or regulation, such description must be 269 | sufficiently detailed for a recipient of ordinary skill to be able to 270 | understand it. 271 | 272 | 5. Application of this License. 273 | 274 | This License applies to code to which the Initial Developer has 275 | attached the notice in Exhibit A and to related Covered Code. 276 | 277 | 6. Versions of the License. 278 | 279 | 6.1. New Versions. 280 | Netscape Communications Corporation ("Netscape") may publish revised 281 | and/or new versions of the License from time to time. Each version 282 | will be given a distinguishing version number. 283 | 284 | 6.2. Effect of New Versions. 285 | Once Covered Code has been published under a particular version of the 286 | License, You may always continue to use it under the terms of that 287 | version. You may also choose to use such Covered Code under the terms 288 | of any subsequent version of the License published by Netscape. No one 289 | other than Netscape has the right to modify the terms applicable to 290 | Covered Code created under this License. 291 | 292 | 6.3. Derivative Works. 293 | If You create or use a modified version of this License (which you may 294 | only do in order to apply it to code which is not already Covered Code 295 | governed by this License), You must (a) rename Your license so that 296 | the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", 297 | "MPL", "NPL" or any confusingly similar phrase do not appear in your 298 | license (except to note that your license differs from this License) 299 | and (b) otherwise make it clear that Your version of the license 300 | contains terms which differ from the Mozilla Public License and 301 | Netscape Public License. (Filling in the name of the Initial 302 | Developer, Original Code or Contributor in the notice described in 303 | Exhibit A shall not of themselves be deemed to be modifications of 304 | this License.) 305 | 306 | 7. DISCLAIMER OF WARRANTY. 307 | 308 | COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, 309 | WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, 310 | WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF 311 | DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. 312 | THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE 313 | IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, 314 | YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE 315 | COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER 316 | OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF 317 | ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 318 | 319 | 8. TERMINATION. 320 | 321 | 8.1. This License and the rights granted hereunder will terminate 322 | automatically if You fail to comply with terms herein and fail to cure 323 | such breach within 30 days of becoming aware of the breach. All 324 | sublicenses to the Covered Code which are properly granted shall 325 | survive any termination of this License. Provisions which, by their 326 | nature, must remain in effect beyond the termination of this License 327 | shall survive. 328 | 329 | 8.2. If You initiate litigation by asserting a patent infringement 330 | claim (excluding declaratory judgment actions) against Initial Developer 331 | or a Contributor (the Initial Developer or Contributor against whom 332 | You file such action is referred to as "Participant") alleging that: 333 | 334 | (a) such Participant's Contributor Version directly or indirectly 335 | infringes any patent, then any and all rights granted by such 336 | Participant to You under Sections 2.1 and/or 2.2 of this License 337 | shall, upon 60 days notice from Participant terminate prospectively, 338 | unless if within 60 days after receipt of notice You either: (i) 339 | agree in writing to pay Participant a mutually agreeable reasonable 340 | royalty for Your past and future use of Modifications made by such 341 | Participant, or (ii) withdraw Your litigation claim with respect to 342 | the Contributor Version against such Participant. If within 60 days 343 | of notice, a reasonable royalty and payment arrangement are not 344 | mutually agreed upon in writing by the parties or the litigation claim 345 | is not withdrawn, the rights granted by Participant to You under 346 | Sections 2.1 and/or 2.2 automatically terminate at the expiration of 347 | the 60 day notice period specified above. 348 | 349 | (b) any software, hardware, or device, other than such Participant's 350 | Contributor Version, directly or indirectly infringes any patent, then 351 | any rights granted to You by such Participant under Sections 2.1(b) 352 | and 2.2(b) are revoked effective as of the date You first made, used, 353 | sold, distributed, or had made, Modifications made by that 354 | Participant. 355 | 356 | 8.3. If You assert a patent infringement claim against Participant 357 | alleging that such Participant's Contributor Version directly or 358 | indirectly infringes any patent where such claim is resolved (such as 359 | by license or settlement) prior to the initiation of patent 360 | infringement litigation, then the reasonable value of the licenses 361 | granted by such Participant under Sections 2.1 or 2.2 shall be taken 362 | into account in determining the amount or value of any payment or 363 | license. 364 | 365 | 8.4. In the event of termination under Sections 8.1 or 8.2 above, 366 | all end user license agreements (excluding distributors and resellers) 367 | which have been validly granted by You or any distributor hereunder 368 | prior to termination shall survive termination. 369 | 370 | 9. LIMITATION OF LIABILITY. 371 | 372 | UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT 373 | (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL 374 | DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, 375 | OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR 376 | ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY 377 | CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, 378 | WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER 379 | COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN 380 | INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF 381 | LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY 382 | RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW 383 | PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE 384 | EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO 385 | THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 386 | 387 | 10. U.S. GOVERNMENT END USERS. 388 | 389 | The Covered Code is a "commercial item," as that term is defined in 390 | 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer 391 | software" and "commercial computer software documentation," as such 392 | terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 393 | C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), 394 | all U.S. Government End Users acquire Covered Code with only those 395 | rights set forth herein. 396 | 397 | 11. MISCELLANEOUS. 398 | 399 | This License represents the complete agreement concerning subject 400 | matter hereof. If any provision of this License is held to be 401 | unenforceable, such provision shall be reformed only to the extent 402 | necessary to make it enforceable. This License shall be governed by 403 | California law provisions (except to the extent applicable law, if 404 | any, provides otherwise), excluding its conflict-of-law provisions. 405 | With respect to disputes in which at least one party is a citizen of, 406 | or an entity chartered or registered to do business in the United 407 | States of America, any litigation relating to this License shall be 408 | subject to the jurisdiction of the Federal Courts of the Northern 409 | District of California, with venue lying in Santa Clara County, 410 | California, with the losing party responsible for costs, including 411 | without limitation, court costs and reasonable attorneys' fees and 412 | expenses. The application of the United Nations Convention on 413 | Contracts for the International Sale of Goods is expressly excluded. 414 | Any law or regulation which provides that the language of a contract 415 | shall be construed against the drafter shall not apply to this 416 | License. 417 | 418 | 12. RESPONSIBILITY FOR CLAIMS. 419 | 420 | As between Initial Developer and the Contributors, each party is 421 | responsible for claims and damages arising, directly or indirectly, 422 | out of its utilization of rights under this License and You agree to 423 | work with Initial Developer and Contributors to distribute such 424 | responsibility on an equitable basis. Nothing herein is intended or 425 | shall be deemed to constitute any admission of liability. 426 | 427 | 13. MULTIPLE-LICENSED CODE. 428 | 429 | Initial Developer may designate portions of the Covered Code as 430 | "Multiple-Licensed". "Multiple-Licensed" means that the Initial 431 | Developer permits you to utilize portions of the Covered Code under 432 | Your choice of the MPL or the alternative licenses, if any, specified 433 | by the Initial Developer in the file described in Exhibit A. 434 | 435 | EXHIBIT A -Mozilla Public License. 436 | 437 | ``The contents of this file are subject to the Mozilla Public License 438 | Version 1.1 (the "License"); you may not use this file except in 439 | compliance with the License. You may obtain a copy of the License at 440 | https://www.mozilla.org/MPL/ 441 | 442 | Software distributed under the License is distributed on an "AS IS" 443 | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the 444 | License for the specific language governing rights and limitations 445 | under the License. 446 | 447 | The Original Code is RabbitMQ. 448 | 449 | The Initial Developer of the Original Code is Pivotal Software, Inc. 450 | Copyright (c) 2017 Pivotal Software, Inc. All rights reserved. 451 | 452 | Alternatively, the contents of this file may be used under the terms 453 | of the GNU General Public License version 2 (the "GPL2"), or 454 | the Apache License version 2 (the "ASL2") in which case the 455 | provisions of GPL2 or the ASL2 are applicable instead of those 456 | above. If you wish to allow use of your version of this file only 457 | under the terms of the GPL2 or the ASL2 and not to allow others to use 458 | your version of this file under the MPL, indicate your decision by 459 | deleting the provisions above and replace them with the notice and 460 | other provisions required by the GPL2 or the ASL2. If you do not delete 461 | the provisions above, a recipient may use your version of this file 462 | under either the MPL, the GPL2 or the ASL2.'' 463 | 464 | [NOTE: The text of this Exhibit A may differ slightly from the text of 465 | the notices in the Source Code files of the Original Code. You should 466 | use the text of this Exhibit A rather than the text found in the 467 | Original Code Source Code for Your Modifications.] 468 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | module( 2 | name = "com_github_rabbitmq_looking_glass", 3 | version = "0.2.2", 4 | ) 5 | 6 | bazel_dep( 7 | name = "platforms", 8 | version = "0.0.6", 9 | ) 10 | 11 | external_deps = use_extension( 12 | ":extensions.bzl", 13 | "external_deps", 14 | ) 15 | 16 | use_repo( 17 | external_deps, 18 | "nif_helpers", 19 | ) 20 | 21 | bazel_dep( 22 | name = "rules_erlang", 23 | version = "3.15.2", 24 | ) 25 | 26 | erlang_config = use_extension( 27 | "@rules_erlang//bzlmod:extensions.bzl", 28 | "erlang_config", 29 | dev_dependency = True, 30 | ) 31 | 32 | erlang_config.internal_erlang_from_github_release( 33 | name = "24", 34 | sha256 = "0bf449184ef4ca71f9af79fc086d941f4532922e01957e84a4fec192c2db5c0c", 35 | version = "24.3.4.17", 36 | ) 37 | 38 | erlang_config.internal_erlang_from_github_release( 39 | name = "25", 40 | sha256 = "fd690c843100c3268ac7d8ada02af8310e8520bf32581eb72f28c26ae61b46ad", 41 | version = "25.3.2.12", 42 | ) 43 | 44 | erlang_config.internal_erlang_from_github_release( 45 | name = "26", 46 | sha256 = "47853ea9230643a0a31004433f07a71c1b92d6e0094534f629e3b75dbc62f193", 47 | version = "26.0.2", 48 | ) 49 | 50 | use_repo( 51 | erlang_config, 52 | "erlang_config", 53 | ) 54 | 55 | erlang_dev_package = use_extension( 56 | "@rules_erlang//bzlmod:extensions.bzl", 57 | "erlang_package", 58 | dev_dependency = True, 59 | ) 60 | 61 | erlang_dev_package.git_package( 62 | testonly = True, 63 | branch = "master", 64 | repository = "extend/ct_helper", 65 | ) 66 | 67 | use_repo( 68 | erlang_dev_package, 69 | "ct_helper", 70 | ) 71 | 72 | bazel_dep( 73 | name = "lz4-erlang", 74 | version = "1.9.4.1", 75 | ) 76 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = looking_glass 2 | PROJECT_DESCRIPTION = New project 3 | PROJECT_VERSION = 0.2.2 4 | 5 | DEPS = lz4 6 | dep_lz4 = git https://github.com/rabbitmq/lz4-erlang.git main 7 | 8 | BUILD_DEPS = nif_helpers 9 | dep_nif_helpers = git https://github.com/ninenines/nif_helpers.git ead6adc15fca3c314351523080d2ccb1956d5956 10 | DEP_PLUGINS = nif_helpers 11 | 12 | LOCAL_DEPS = runtime_tools 13 | 14 | C_SRC_OUTPUT ?= $(CURDIR)/priv/lg_tracer 15 | 16 | TEST_DEPS = ct_helper 17 | dep_ct_helper = git https://github.com/ninenines/ct_helper master 18 | 19 | TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}' 20 | 21 | include erlang.mk 22 | 23 | # CT_OPTS += -boot start_sasl 24 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | = Looking Glass 2 | 3 | Looking Glass is a tracer and profiler for Erlang/OTP. 4 | 5 | == Goals 6 | 7 | Looking Glass is the next generation profiling tool. It 8 | is implemented as an `erl_tracer` NIF and thus requires 9 | Erlang/OTP 22.0 or above. 10 | 11 | Looking Glass aims to provide a very efficient tool 12 | usable both in development and production settings, 13 | and capable of running for a long amount of time 14 | even on busy systems. 15 | 16 | == Online documentation 17 | 18 | * link:/doc/src/guide/book.asciidoc[User guide] 19 | 20 | == Offline documentation 21 | 22 | * While still online, run `make docs` 23 | * User guide available in `doc/` in PDF and HTML formats 24 | * Function reference man pages available in `doc/man3/` and `doc/man7/` 25 | * Run `make install-docs` to install man pages on your system 26 | * Full documentation in Asciidoc available in `doc/src/` 27 | * Examples available in `examples/` 28 | -------------------------------------------------------------------------------- /WORKSPACE.bazel: -------------------------------------------------------------------------------- 1 | workspace(name = "com_github_rabbitmq_looking_glass") 2 | 3 | load( 4 | "@bazel_tools//tools/build_defs/repo:http.bzl", 5 | "http_archive", 6 | ) 7 | load( 8 | "@bazel_tools//tools/build_defs/repo:git.bzl", 9 | "git_repository", 10 | ) 11 | 12 | http_archive( 13 | name = "io_buildbuddy_buildbuddy_toolchain", 14 | sha256 = "48546946879b1fd2dcba327ba15776c822f2ce9a9ef1077be9bf3ecadcc1564a", 15 | strip_prefix = "buildbuddy-toolchain-b2f5e7e3b126c6d7cf243227147478c0959bfc95", 16 | urls = ["https://github.com/buildbuddy-io/buildbuddy-toolchain/archive/b2f5e7e3b126c6d7cf243227147478c0959bfc95.zip"], 17 | ) 18 | 19 | load("@io_buildbuddy_buildbuddy_toolchain//:deps.bzl", "buildbuddy_deps") 20 | 21 | buildbuddy_deps() 22 | 23 | load("@io_buildbuddy_buildbuddy_toolchain//:rules.bzl", "buildbuddy") 24 | 25 | buildbuddy( 26 | name = "buildbuddy_toolchain", 27 | llvm = True, 28 | ) 29 | 30 | git_repository( 31 | name = "rbe", 32 | branch = "linux-rbe", 33 | remote = "https://github.com/rabbitmq/rbe-erlang-platform.git", 34 | ) 35 | -------------------------------------------------------------------------------- /c_src/lg_tracer.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | // 3 | // This package, Looking Glass, is double-licensed under the Mozilla 4 | // Public License 1.1 ("MPL") and the Apache License version 2 5 | // ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | // please see LICENSE-APACHE2. 7 | // 8 | // This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | // either express or implied. See the LICENSE file for specific language governing 10 | // rights and limitations of this software. 11 | // 12 | // If you have any questions regarding licensing, please contact us at 13 | // info@rabbitmq.com. 14 | 15 | #define NIF_FUNCTION_NAME(f) lg_ ## f 16 | 17 | #include "nif_helpers.h" 18 | 19 | // List of atoms used by this NIF. 20 | // 21 | // @todo We don't use threads so perhaps we should make nif_helpers 22 | // better by splitting concerns into threads/not and have nif_helpers 23 | // decide when to create the _nif_thread_ret atom or not. 24 | 25 | #define NIF_ATOMS(A) \ 26 | A(_nif_thread_ret_) \ 27 | A(call) \ 28 | A(closed) \ 29 | A(cpu_timestamp) \ 30 | A(discard) \ 31 | A(exception_from) \ 32 | A(exit) \ 33 | A(extra) \ 34 | A(gc_major_end) \ 35 | A(gc_major_start) \ 36 | A(gc_minor_end) \ 37 | A(gc_minor_start) \ 38 | A(getting_linked) \ 39 | A(getting_unlinked) \ 40 | A(in) \ 41 | A(in_exiting) \ 42 | A(link) \ 43 | A(match_spec_result) \ 44 | A(mode) \ 45 | A(monotonic) \ 46 | A(ok) \ 47 | A(open) \ 48 | A(out) \ 49 | A(out_exited) \ 50 | A(out_exiting) \ 51 | A(percent) \ 52 | A(profile) \ 53 | A(receive) \ 54 | A(register) \ 55 | A(remove) \ 56 | A(return_from) \ 57 | A(return_to) \ 58 | A(scheduler_id) \ 59 | A(send) \ 60 | A(send_to_non_existing_process) \ 61 | A(spawn) \ 62 | A(spawned) \ 63 | A(strict_monotonic) \ 64 | A(timestamp) \ 65 | A(trace) \ 66 | A(trace_status) \ 67 | A(tracers) \ 68 | A(unlink) \ 69 | A(unregister) 70 | 71 | NIF_ATOMS(NIF_ATOM_DECL) 72 | 73 | // List of functions defined in this NIF. 74 | 75 | #define NIF_FUNCTIONS(F) \ 76 | F(enabled, 3) \ 77 | F(enabled_call, 3) \ 78 | F(enabled_procs, 3) \ 79 | F(enabled_running_procs, 3) \ 80 | F(enabled_send, 3) \ 81 | F(trace, 5) 82 | 83 | NIF_FUNCTIONS(NIF_FUNCTION_H_DECL) 84 | 85 | static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) 86 | { 87 | NIF_ATOMS(NIF_ATOM_INIT) 88 | 89 | return 0; 90 | } 91 | 92 | static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) 93 | { 94 | *priv_data = *old_priv_data; 95 | 96 | return 0; 97 | } 98 | 99 | static void unload(ErlNifEnv* env, void* priv_data) 100 | { 101 | } 102 | 103 | // enabled(TraceTag, TracerState, Tracee) 104 | 105 | NIF_FUNCTION(enabled) 106 | { 107 | ERL_NIF_TERM tracers, value; 108 | ErlNifPid tracer; 109 | 110 | // @todo We can go one step further by having the one pid 111 | // in its own value in the map, skipping a get_map_value step. 112 | 113 | // This function will only be called for trace_status. 114 | // We can take a few shortcuts knowing this. 115 | 116 | // Disable the trace when the tracers option is missing. 117 | if (!enif_get_map_value(env, argv[1], atom_tracers, &tracers)) 118 | return atom_remove; 119 | 120 | // Because the tracers supervisor is a one_for_all, we only need 121 | // to check one of the tracer processes to confirm all are alive. 122 | 123 | // We know for a fact that this key exists because 124 | // there's at least one tracer process. 125 | enif_get_map_value(env, tracers, enif_make_int(env, 0), &value); 126 | 127 | // Disable the trace when one of the tracers is not a local process. 128 | if (!enif_get_local_pid(env, value, &tracer)) 129 | return atom_remove; 130 | 131 | // Disable the trace when one of the tracers is not alive. 132 | if (!enif_is_process_alive(env, &tracer)) 133 | return atom_remove; 134 | 135 | return atom_discard; 136 | } 137 | 138 | NIF_FUNCTION(enabled_call) 139 | { 140 | // We always want both call and return_to. 141 | return atom_trace; 142 | } 143 | 144 | NIF_FUNCTION(enabled_procs) 145 | { 146 | ERL_NIF_TERM mode; 147 | 148 | // We only want the spawn and exit events when 'profile' mode 149 | // is enabled. Technically we only care about exits for callgrind, 150 | // but spawn is cheap to keep and useful for message profilers. 151 | if (enif_get_map_value(env, argv[1], atom_mode, &mode) 152 | && enif_is_identical(atom_profile, mode) 153 | && !(enif_is_identical(atom_spawn, argv[0]) 154 | || enif_is_identical(atom_exit, argv[0]))) { 155 | return atom_discard; 156 | } 157 | 158 | return atom_trace; 159 | } 160 | 161 | NIF_FUNCTION(enabled_running_procs) 162 | { 163 | // We always want both in and out. 164 | return atom_trace; 165 | } 166 | 167 | NIF_FUNCTION(enabled_send) 168 | { 169 | // We always want both send and send_to_non_existing_process. 170 | return atom_trace; 171 | } 172 | 173 | // trace(TraceTag, TracerState, Tracee, TraceTerm, Opts) 174 | 175 | NIF_FUNCTION(trace) 176 | { 177 | ERL_NIF_TERM tracers, head, ts, extra, mspec, msg; 178 | ErlNifPid tracer; 179 | unsigned int nth; 180 | size_t len; 181 | int has_extra, has_mspec; 182 | 183 | if (!enif_get_map_value(env, argv[1], atom_tracers, &tracers)) 184 | return atom_ok; 185 | 186 | // We know for a fact that the argument is a map. And if not, 187 | // no problem because we will return when trying to get a value from it. 188 | enif_get_map_size(env, tracers, &len); 189 | 190 | #if (ERL_NIF_MAJOR_VERSION >= 2) && (ERL_NIF_MINOR_VERSION >= 12) 191 | nth = enif_hash(ERL_NIF_INTERNAL_HASH, argv[2], 0) % len; 192 | #else 193 | // Select the correct tracer for this process. 194 | // 195 | // The pid value is detailed in: 196 | // 5b6dd0e84cf0f1dc19ddd05f86cf04b2695d8a9e/erts/emulator/beam/erl_term.h#L498 197 | // 198 | // As can be seen there, the first four bits of the pid value 199 | // are always the same. We therefore shift them out. 200 | 201 | ErlNifPid tracee; 202 | 203 | if (!enif_get_local_pid(env, argv[2], &tracee)) 204 | return atom_ok; 205 | 206 | nth = (tracee.pid >> 4) % len; 207 | #endif 208 | 209 | if (!enif_get_map_value(env, tracers, enif_make_int(env, nth), &head)) 210 | return atom_ok; 211 | 212 | if (!enif_get_local_pid(env, head, &tracer)) 213 | return atom_ok; 214 | 215 | // Everything good. Generate a timestamp to include in the message. 216 | 217 | ts = enif_make_int64(env, enif_monotonic_time(ERL_NIF_USEC)); 218 | 219 | // Build the message. There can be two different messages 220 | // depending on whether the extra option was set: 221 | // 222 | // - {Tag, Tracee, Ts, Term} 223 | // - {Tag, Tracee, Ts, Term, Extra} 224 | // 225 | // On top of that when match specs are enabled we may have 226 | // one additional term at the end of the tuple containing 227 | // the result of the match spec function. 228 | // 229 | // - {Tag, Tracee, Ts, Term, Result} 230 | // - {Tag, Tracee, Ts, Term, Extra, Result} 231 | 232 | has_extra = enif_get_map_value(env, argv[4], atom_extra, &extra); 233 | has_mspec = enif_get_map_value(env, argv[4], atom_match_spec_result, &mspec); 234 | 235 | if (has_extra && has_mspec) 236 | msg = enif_make_tuple6(env, argv[0], argv[2], ts, argv[3], extra, mspec); 237 | else if (has_extra) 238 | msg = enif_make_tuple5(env, argv[0], argv[2], ts, argv[3], extra); 239 | else if (has_mspec) 240 | msg = enif_make_tuple5(env, argv[0], argv[2], ts, argv[3], mspec); 241 | else 242 | msg = enif_make_tuple4(env, argv[0], argv[2], ts, argv[3]); 243 | 244 | // Send the message to the selected tracer. 245 | 246 | enif_send(env, &tracer, NULL, msg); 247 | 248 | return atom_ok; 249 | } 250 | 251 | static ErlNifFunc nif_funcs[] = { 252 | NIF_FUNCTIONS(NIF_FUNCTION_ARRAY) 253 | }; 254 | 255 | ERL_NIF_INIT(lg_tracer, nif_funcs, load, NULL, upgrade, unload) 256 | -------------------------------------------------------------------------------- /doc/src/guide/book.asciidoc: -------------------------------------------------------------------------------- 1 | = Looking Glass User Guide 2 | 3 | include::introduction.asciidoc[Introduction] 4 | 5 | include::tracing.asciidoc[Tracing] 6 | 7 | include::callgrind.asciidoc[Callgrind profiling] 8 | 9 | include::flame.asciidoc[Flame graph profiling] 10 | 11 | include::messages.asciidoc[Messages profiling] 12 | -------------------------------------------------------------------------------- /doc/src/guide/callgrind.asciidoc: -------------------------------------------------------------------------------- 1 | [[callgrind]] 2 | == Callgrind profiling 3 | 4 | Looking Glass' primary purpose is the profiling of 5 | Erlang applications. This is done by first tracing 6 | events to a file or socket and then processing it 7 | to extract useful output. 8 | 9 | Profiling tools generally have a few different types 10 | of output. This chapter is about callgrind output, 11 | which can be read using the `qcachegrind`/`kcachegrind` 12 | tool. 13 | 14 | === Quick start 15 | 16 | Assuming you generated trace files using the profile 17 | mode and the running flag, as detailed in the 18 | xref:tracing_running[Tracing chapter], you can 19 | generate callgrind.out files using the following 20 | command: 21 | 22 | [source,erlang] 23 | ---- 24 | 1> lg_callgrind:profile_many("traces.lz4.*", "callgrind.out", 25 | #{running => true}). 26 | ---- 27 | 28 | This will create a callgrind.out file for all trace files 29 | you generated. For example if you had 'traces.lz4.1' and 30 | 'traces.lz4.2', you should now also have 'callgrind.out.1' 31 | and 'callgrind.out.2'. 32 | 33 | You can now open these two files in the cachegrind tool, 34 | either from the user interface or from the command line: 35 | 36 | [source,bash] 37 | ---- 38 | $ qcachegrind callgrind.out 39 | ---- 40 | 41 | It will automatically detect and open all files matching 42 | the `callgrind.out.*` pattern. 43 | 44 | === Profiling one file 45 | 46 | You can profile one file by calling the function 47 | `lg_callgrind:profile/2,3`. It takes the trace file name, 48 | the output file name and an optional map of options: 49 | 50 | [source,erlang] 51 | ---- 52 | 1> lg_callgrind:profile("traces.lz4.1", "callgrind.out.1"). 53 | ---- 54 | 55 | It also accepts options: 56 | 57 | [source,erlang] 58 | ---- 59 | 1> lg_callgrind:profile("traces.lz4.1", "callgrind.out.1", 60 | #{running => true}). 61 | ---- 62 | 63 | === Profiling many files 64 | 65 | A convenience function is available for profiling many 66 | files at once: `lg_callgrind:profile_many/2,3`. It takes 67 | a wildcard pattern as first argument and a file name 68 | prefix as second argument: 69 | 70 | [source,erlang] 71 | ---- 72 | 1> lg_callgrind:profile_many("traces.lz4.*", "callgrind.out"). 73 | ---- 74 | 75 | If there were two trace files, this will result in two 76 | 'callgrind.out' files: 'callgrind.out.1' and 'callgrind.out.2'. 77 | 78 | It also accepts options: 79 | 80 | [source,erlang] 81 | ---- 82 | 1> lg_callgrind:profile_many("traces.lz4.*", "callgrind.out", 83 | #{running => true}). 84 | ---- 85 | 86 | === Running information 87 | 88 | When the trace files contain running information, meaning 89 | they were created with the `running` flag enabled, you 90 | need to also pass the `running` flag to the profiler in 91 | order to have that information available in 'callgrind.out' 92 | files: 93 | 94 | [source,erlang] 95 | ---- 96 | 1> lg_callgrind:profile_many("traces.lz4.*", "callgrind.out", 97 | #{running => true}). 98 | ---- 99 | 100 | === Scope 101 | 102 | By default the scope of the trace events is global. This 103 | means that the cachegrind tool will group all events 104 | together regardless of where they happened. This is 105 | useful to see which functions take the most resources 106 | overall. 107 | 108 | Other times you may want to see which *processes* take 109 | the most resources. To do this you need to instruct 110 | Looking Glass to keep the process information when 111 | generating the 'callgrind.out' files. This is done 112 | using the `scope` option: 113 | 114 | [source,erlang] 115 | ---- 116 | 1> lg_callgrind:profile_many("traces.lz4.*", "callgrind.out", 117 | #{scope => per_process}). 118 | ---- 119 | 120 | === Using the cachegrind tool 121 | 122 | There are a few gotchas to be aware of when using the 123 | cachegrind tool with the output generated by Looking Glass. 124 | 125 | The cachegrind tool was built with imperative code in mind. 126 | It does not deal too well with recursion. This means that 127 | the number of times functions are called might not always 128 | be correct, especially for functions that call themselves. 129 | You can see an example of this issue when looking at the 130 | call graph, for example. 131 | 132 | Looking Glass uses ELF Object field for storing the pid of 133 | the process when the `scope => per_process` option is used. 134 | This allows you to investigate processes individually by 135 | using the 'grouping' feature and selecting 'ELF Object'. 136 | You can then see which processes take the most resources 137 | and look at the function calls within those processes. 138 | 139 | When the running flag is used, the following event types 140 | are generated: 141 | 142 | * Total time in microseconds 143 | * Active time in microseconds 144 | * Wait time in microseconds (scheduled out) 145 | * Number of times the process was scheduled out 146 | 147 | The following formula is true: `Total = Active + Wait`. 148 | 149 | The wait time is the time spent when the process was 150 | scheduled out, in other words it was not running. This 151 | happens in a number of different places, like receive 152 | clauses or when the reduction count reached zero. 153 | 154 | The number of times the process was scheduled out may 155 | or may not be accurate at this time. Another part that 156 | may not be accurate is the time spent doing port 157 | operations which may appear as active time when the 158 | process is mostly waiting. Both will be improved 159 | in the future. 160 | 161 | While Looking Glass provides line number information 162 | about the various calls, it is not able to identify 163 | which function clause was involved during this call. 164 | This means that the call information for functions 165 | with a lot of clauses will get aggregated at the same 166 | line number when looking at the source code in the 167 | cachegrind tool. This has an important impact on 168 | most standard behaviors, including `handle_event` 169 | from `gen_statem`. You can however structure your 170 | code so that clause-heavy functions only dispatch 171 | to other functions, in turn getting a better view 172 | in the cachegrind tool. 173 | 174 | Looking Glass is not able to find the line number 175 | of list comprehensions and funs at this time. They 176 | will always point to line number 1. 177 | -------------------------------------------------------------------------------- /doc/src/guide/flame.asciidoc: -------------------------------------------------------------------------------- 1 | [[flame]] 2 | == Flame graph profiling 3 | 4 | As an alternative to xref:callgrind[Callgrind output], 5 | Looking Glass provides flame graphs. Flame graphs are 6 | a graphical view of stack traces that make it obvious 7 | where the most time is spent. It complements the other 8 | graphical views provided by `qcachegrind`. 9 | 10 | Looking Glass only takes care of providing an output 11 | that can then be converted into a flame graph using 12 | the usual tool (not included). This chapter will 13 | explain both operations. 14 | 15 | === Required trace options 16 | 17 | In order to generate a flame graph we currently need to 18 | use one additional option when tracing. This option will 19 | result in adding stack trace information to call events. 20 | The option is `process_dump` and it must be set to `true`. 21 | 22 | To give an example, instead of this: 23 | 24 | [source,erlang] 25 | ---- 26 | 1> lg:trace('_', lg_file_tracer, "traces.lz4"). 27 | ---- 28 | 29 | Do this: 30 | 31 | [source,erlang] 32 | ---- 33 | 1> lg:trace('_', lg_file_tracer, "traces.lz4", 34 | #{process_dump => true}). 35 | ---- 36 | 37 | === Profiling one file 38 | 39 | The `lg_flame` module provides a similar interface as other 40 | Looking Glass profilers. You can produce an intermediate 41 | output based on one or many files. 42 | 43 | To profile one file: 44 | 45 | [source,erlang] 46 | ---- 47 | 1> lg_flame:profile("traces.lz4.1", "output"). 48 | ---- 49 | 50 | This will create an intermediate file named 'output'. 51 | 52 | === Profiling many files 53 | 54 | To profile many files: 55 | 56 | [source,erlang] 57 | ---- 58 | 1> lg_flame:profile_many("traces.lz4.*", "output"). 59 | ---- 60 | 61 | Note that the output is always a single file as the 62 | results are merged together. 63 | 64 | === Building the flame graph 65 | 66 | https://github.com/brendangregg/FlameGraph[flamegraph.pl] 67 | can be used to produce actual SVG flame graphs. 68 | 69 | First we need to clone it. Anywhere will do: 70 | 71 | [source,bash] 72 | $ git clone https://github.com/brendangregg/FlameGraph 73 | 74 | Then we can use it on our output file to create an SVG: 75 | 76 | [source,bash] 77 | $ ./FlameGraph/flamegraph.pl output > output.svg 78 | 79 | You can then open the output SVG in your Web browser 80 | of choice. The produced SVG is interactive, you can 81 | click on the different functions to zoom in, and you 82 | can also search for a specific function call. 83 | -------------------------------------------------------------------------------- /doc/src/guide/introduction.asciidoc: -------------------------------------------------------------------------------- 1 | [[introduction]] 2 | == Introduction 3 | 4 | Looking Glass is a tracer and profiler for Erlang/OTP. 5 | 6 | Looking Glass is the next generation profiling tool. It 7 | is implemented as an `erl_tracer` NIF and thus requires 8 | Erlang/OTP 19.0 or above. 9 | 10 | Looking Glass aims to provide a very efficient tool 11 | usable both in development and production settings, 12 | and capable of running for a very long amount of time 13 | even on busy systems. 14 | 15 | === Supported platforms 16 | 17 | Looking Glass is currently developed on Linux but should 18 | also work on OSX and Windows. 19 | 20 | Looking Glass requires Erlang/OTP 19.0 or above. 21 | 22 | A cachegrind tool is required for reading the output 23 | from `lg_callgrind`. The `qcachegrind` tool (also 24 | known as `kcachegrind`) is recommended. Note that 25 | it is a good idea to also install `graphviz` to 26 | have the quite informative call graphs. 27 | 28 | === Requirements 29 | 30 | Looking Glass requires a C compiler toolchain and an `lz4` library to be installed. 31 | 32 | === License 33 | 34 | Looking Glass is double-licensed under the Mozilla 35 | Public License 1.1 and the Apache License version 2. 36 | 37 | See the LICENSE file for more information. 38 | 39 | === Versioning 40 | 41 | Looking Glass uses https://semver.org/[Semantic Versioning 2.0.0]. 42 | -------------------------------------------------------------------------------- /doc/src/guide/messages.asciidoc: -------------------------------------------------------------------------------- 1 | [[messages]] 2 | == Messages profiling 3 | 4 | Looking Glass can also be used to profile Erlang processes 5 | based on the messages they send. It can help you detect 6 | which processes are the most busy and is able to generate 7 | graphs and sequence diagrams to help you debug complex 8 | issues. 9 | 10 | === Enabling the tracing of messages 11 | 12 | By default Looking Glass will not include the messages 13 | in the trace files. It needs to be enabled through the 14 | xref:tracing_send[send option]. 15 | 16 | The output from one tracing session can then be used 17 | for both callgrind and message profiling. 18 | 19 | === Profiling one file 20 | 21 | You can profile one file by calling the function 22 | `lg_messages:profile/1`. It takes the trace file name 23 | and prints out the result of the profiling. 24 | 25 | [source,erlang] 26 | ---- 27 | 1> lg_messages:profile("traces.lz4.1"). 28 | ---- 29 | 30 | It will also create a GraphViz file currently hardcoded as 31 | 'digraph.gv' and print further instructions to use it. 32 | 33 | === Profiling many files 34 | 35 | A convenience function is available for profiling many 36 | files at once: `lg_callgrind:profile_many/2,3`. It takes 37 | a wildcard pattern as first argument and a file name 38 | prefix as second argument: 39 | 40 | You can profile many files by calling the function 41 | `lg_messages:profile_many/1`. It takes a wildcard pattern 42 | and prints out the result of the profiling. The result 43 | is a merge of the events in the different trace files. 44 | 45 | [source,erlang] 46 | ---- 47 | 1> lg_messages:profile_many("traces.lz4.*"). 48 | ---- 49 | 50 | === Profile output 51 | 52 | The profile step will result in four tables being printed. 53 | 54 | * The first table shows the processes that sent the most messages. 55 | 56 | * The second table shows the processes that sent the most messages 57 | to processes that are either dead or never existed in the first place. 58 | 59 | * The third table shows the processes that were most frequently sending 60 | messages to one specific other process (from Alice to Bob). 61 | 62 | * The last table shows the processes that were exchanging the most 63 | messages (from Alice to Bob, and from Bob to Alice). 64 | 65 | .Example output 66 | ---- 67 | 1> lg_messages:profile_many("traces.lz4.*"). 68 | 69 | They sent the most messages 70 | =========================== 71 | 72 | Process ID Count Most recent message 73 | ---------- ----- ------------------- 74 | <7782.367.0> 147327 {notify,{event,channel_closed,...}} 75 | <7782.356.0> 73035 {notify,{event,connection_closed,...}} 76 | <7782.382.0> 30514 pause 77 | <7782.391.0> 30052 {'$gen_cast',{deliver,{...},...}} 78 | <7782.365.0> 1486 {channel_exit,1,{writer,...}} 79 | [...] 80 | 81 | They sent the most messages to dead processes 82 | ============================================= 83 | 84 | Process ID Count Most recent message 85 | ---------- ----- ------------------- 86 | <7782.367.0> 29 {notify,{event,channel_closed,...}} 87 | 88 | They sent the most messages to one other process 89 | ================================================ 90 | 91 | From pid To pid Count Most recent message 92 | -------- ------ ----- ------------------- 93 | <7782.367.0> <7782.365.0> 74318 {notify,{event,channel_closed,...}} 94 | <7782.356.0> <7782.367.0> 73001 {notify,{event,connection_closed,...}} 95 | <7782.367.0> <7782.375.0> 73000 {notify,{event,channel_closed,...}} 96 | <7782.382.0> <7782.391.0> 30202 pause 97 | <7782.391.0> <7782.375.0> 29894 {'$gen_cast',{deliver,{...},...}} 98 | <7782.365.0> <7782.375.0> 1485 {channel_exit,1,{writer,...}} 99 | [...] 100 | 101 | They sent the most messages to each other 102 | ========================================= 103 | 104 | Count Pid 1 Most recent message 105 | Pid 2 from the corresponding process 106 | ----- ----- ------------------------------ 107 | 74318 <7782.365.0> {channel_exit,1,{writer,...}} 108 | <7782.367.0> {notify,{event,channel_closed,...}} 109 | 73001 <7782.356.0> {notify,{event,connection_closed,...}} 110 | <7782.367.0> {notify,{event,channel_closed,...}} 111 | 73000 <7782.367.0> {notify,{event,channel_closed,...}} 112 | <7782.375.0> '' 113 | 30351 <7782.382.0> pause 114 | <7782.391.0> {'$gen_cast',{deliver,{...},...}} 115 | 29894 <7782.375.0> '' 116 | <7782.391.0> {'$gen_cast',{deliver,{...},...}} 117 | [...] 118 | 119 | The file digraph.gv was created. Use GraphViz to make a PNG. 120 | $ dot -Tpng -O digraph.gv 121 | 122 | You can also edit the file to remove uninteresting processes. 123 | One line in the file is equal to a connection between two processes. 124 | ---- 125 | 126 | At the end of the output, instructions are given to generate an 127 | image from a GraphViz file. This image shows the relationships 128 | between the processes and indicates how many messages they send 129 | to each other. 130 | 131 | The file generated by Looking Glass is a text file that can be 132 | further edited as necessary. It looks like this: 133 | 134 | ---- 135 | digraph { 136 | concentrate=true; 137 | splines=ortho; 138 | edge [arrowhead=none, labelfontsize=12.0, minlen=3]; 139 | 140 | "error_logger" -> "<7782.354.0>" [taillabel=0, headlabel=2]; 141 | "<7782.32.0>" -> "<7782.380.0>" [taillabel=0, headlabel=1]; 142 | "<7782.388.0>" -> "<7782.391.0>" [taillabel=0, headlabel=1]; 143 | "error_logger" -> "<7782.355.0>" [taillabel=0, headlabel=4]; 144 | [...] 145 | } 146 | ---- 147 | 148 | It is of course possible to edit this file. You may want to 149 | modify the style attributes, or even remove processes from 150 | the output entirely. 151 | 152 | === Generating sequence diagrams 153 | 154 | Looking Glass can also be used to extract the sequence of messages 155 | exchanged between two or more processes. This is done using the 156 | `lg_messages_seqdiag` module, which works just like `lg_messages` 157 | except the functions take a second argument containing the list of 158 | pids you wish to investigate. 159 | 160 | To look at one file: 161 | 162 | [source,erlang] 163 | ---- 164 | 1> lg_messages_seqdiag:profile("traces.lz4.1", 165 | ["<7788.381.0>", "<7788.382.0>", "<7774.383.0>", 166 | "<7774.384.0>", "<7774.386.0>"]). 167 | ---- 168 | 169 | And many files: 170 | 171 | [source,erlang] 172 | ---- 173 | 1> lg_messages_seqdiag:profile_many("traces.lz4.*", 174 | ["<7788.381.0>", "<7788.382.0>", "<7774.383.0>", 175 | "<7774.384.0>", "<7774.386.0>"]). 176 | ---- 177 | 178 | The list of pids must be given as a list of strings. This is 179 | because the processes represented do not exist on the running 180 | system. Looking Glass will ignore the node information from the 181 | pid too, so you do not need to worry about it. This explains why 182 | the pids requested in the previous two snippets look as if they 183 | come from different nodes. The pids `"<7888.381.0>"` and 184 | `"<7774.381.0>"` are therefore equivalent. 185 | 186 | After running one of these commands, you will end up with a 187 | file 'seq.diag' that can then be used to create an image. This 188 | file can also be edited later on if necessary. It looks like this: 189 | 190 | ---- 191 | seqdiag { 192 | edge_length = 300; 193 | activation = none; 194 | 195 | "<7774.382.0>" -> "<7774.381.0>" [label="gen:call #1 {start_child,{collector,{rabbit_queue_collector,start_link,[...]},intrinsic,30000,worker,...}}"]; 196 | "<7774.383.0>" -> "<7774.381.0>" [label="{ack,<7774.383.0>,{ok,<7774.383.0>}}"]; 197 | "<7774.381.0>" -> "<7774.382.0>" [label="#1 {ok,<7774.383.0>}"]; 198 | [...] 199 | } 200 | ---- 201 | 202 | Before you can create an image from it, you will need to install 203 | `seqdiag`. Installation instructions will depend on your system. 204 | The project page is at http://blockdiag.com/en/seqdiag/ 205 | 206 | .Example output 207 | image::seq.png[] 208 | 209 | === Identifying processes 210 | 211 | While Looking Glass will display the pid and one sample message 212 | from each process, it's not always ideal to identify which process 213 | is which. 214 | 215 | To allievate that, Looking Glass offers a simple solution: 216 | sending a message to the named process `lg` while a tracer is 217 | running. Looking Glass will inevitably log this message in the 218 | trace file, recognize that the target is `lg` and use the 219 | message as metadata. This metadata is then available to any 220 | module reading from the trace file. 221 | 222 | The process is only available when Looking Glass is running, 223 | of course, which means we can't just send a message directly. 224 | The following works: 225 | 226 | [source,erlang] 227 | ---- 228 | is_pid(whereis(lg)) andalso (lg ! Info). 229 | ---- 230 | 231 | This can be made into a macro, of course: 232 | 233 | [source,erlang] 234 | ---- 235 | %% Store metadata in the trace files when message tracing is enabled. 236 | -define(LG_INFO(Info), is_pid(whereis(lg)) andalso (lg ! Info)). 237 | ---- 238 | 239 | And can then be used like this: 240 | 241 | [source,erlang] 242 | ---- 243 | ?LG_INFO(#{process_type => reader}). 244 | ---- 245 | 246 | The message must always be a map. Reading the trace file 247 | will otherwise fail. Looking Glass only recognizes the 248 | `process_type` field, and uses it as a label to identify 249 | processes when profiling exchanges of messages. You are 250 | free to define any other value you need in the map. 251 | 252 | The metadata can also be updated by sending another message 253 | or by calling the macro a second time. The operation done 254 | on the map will be a merge by default. 255 | -------------------------------------------------------------------------------- /doc/src/guide/seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabbitmq/looking_glass/942c9e1a32d75b22e831091b27829f54f0e57569/doc/src/guide/seq.png -------------------------------------------------------------------------------- /doc/src/guide/tracing.asciidoc: -------------------------------------------------------------------------------- 1 | [[tracing]] 2 | == Tracing 3 | 4 | Looking Glass is both a tracing and a profiling tool. 5 | In this chapter we will take a look at the tracing 6 | capabilities of the tool, and also learn how to create 7 | trace files which are necessary for profiling. 8 | 9 | === First steps 10 | 11 | Let's start by tracing everything. 12 | 13 | Open an Erlang shell and run the following command: 14 | 15 | [source,erlang] 16 | ---- 17 | 1> lg:trace('_'). 18 | {link,<0.4.0>,1488297881224444,#Port<0.692>} 19 | {getting_unlinked,<0.4.0>,1488297881224533,#Port<0.692>} 20 | {link,<0.4.0>,1488297881224640,#Port<0.693>} 21 | {getting_unlinked,<0.4.0>,1488297881224720,#Port<0.693>} 22 | {link,<0.4.0>,1488297881224817,#Port<0.694>} 23 | {getting_unlinked,<0.4.0>,1488297881224881,#Port<0.694>} 24 | {link,<0.4.0>,1488297881224979,#Port<0.695>} 25 | {getting_unlinked,<0.4.0>,1488297881225060,#Port<0.695>} 26 | ... 27 | ---- 28 | 29 | As you can see we get a lot of output. That's because 30 | the `lg:trace/1` function will by default output the 31 | raw trace events to the console. We also used the atom 32 | `'_'` to tell Looking Glass to trace all modules, and 33 | didn't restrict which process should be traced. 34 | 35 | Needless to say, do not do this in production. 36 | 37 | The trace events always come with an event name, the pid 38 | of the process where the event happened, a timestamp in 39 | microseconds and one or two extra elements providing 40 | extra context about the event. 41 | 42 | For example the following event is a function call occurring 43 | in the process `<0.64.0>` at timestamp `1488297891226328` 44 | to `supervisor:handle_info/2`. 45 | 46 | [source,erlang] 47 | ---- 48 | {call,<0.64.0>,1488297891226328,{supervisor,handle_info,2}} 49 | ---- 50 | 51 | === Stop tracing 52 | 53 | To stop tracing, simply call: 54 | 55 | [source,erlang] 56 | ---- 57 | 2> lg:stop(). 58 | ---- 59 | 60 | === Tracing specific modules 61 | 62 | In order to get a more interesting output we need to filter 63 | what will be traced. We may for example only want the events 64 | from the module `shell`: 65 | 66 | [source,erlang] 67 | ---- 68 | 1> lg:trace(shell). 69 | ... 70 | {call,<0.58.0>,1488298545020494,{shell,result_will_be_saved,0}} 71 | {call,<0.58.0>,1488298545020497,{shell,get_history_and_results,0}} 72 | {call,<0.58.0>,1488298545020498,{shell,get_env,2}} 73 | {return_to,<0.58.0>,1488298545020501,{shell,get_history_and_results,0}} 74 | {call,<0.58.0>,1488298545020502,{shell,get_env,2}} 75 | {return_to,<0.58.0>,1488298545020503,{shell,get_history_and_results,0}} 76 | {return_to,<0.58.0>,1488298545020504,{shell,result_will_be_saved,0}} 77 | ... 78 | ---- 79 | 80 | We can also request to trace a list of modules: 81 | 82 | [source,erlang] 83 | ---- 84 | 1> lg:trace([shell, user_drv]). 85 | ... 86 | {call,<0.58.0>,1488299067458321,{shell,record_print_fun,1}} 87 | {return_to,<0.58.0>,1488299067458322,{shell,pp,4}} 88 | {call,<0.58.0>,1488299067458323,{shell,enc,0}} 89 | {call,<0.49.0>,1488299067459603,{user_drv,handle_req,4}} 90 | {call,<0.49.0>,1488299067459605,{user_drv,get_unicode_state,1}} 91 | ... 92 | ---- 93 | 94 | === Tracing applications 95 | 96 | In addition to providing modules, you can provide OTP applications. 97 | When you do so all the modules belonging to the application will 98 | be traced. We can of course trace Looking Glass itself: 99 | 100 | [source,erlang] 101 | ---- 102 | 1> lg:trace({app, looking_glass}). 103 | {link,<0.4.0>,1488299179652509,#Port<0.688>} 104 | {getting_unlinked,<0.4.0>,1488299179652621,#Port<0.688>} 105 | {call,<0.58.0>,1488299179653161,{lg,'-trace_patterns/1-fun-0-',1}} 106 | {call,<0.58.0>,1488299179653164,{lg,trace_pattern,1}} 107 | ... 108 | ---- 109 | 110 | Note that Looking Glass will disable tracing on the tracer processes 111 | themselves (to avoid an infinite recursion). More on that later. 112 | 113 | You can trace any combination of modules and applications: 114 | 115 | [source,erlang] 116 | ---- 117 | 1> lg:trace([shell, {app, looking_glass}]). 118 | ... 119 | ---- 120 | 121 | === Tracing specific processes 122 | 123 | Looking Glass traces all processes by default. 124 | 125 | Large systems tend to have many processes and this can generate 126 | a lot of noise, especially if you are trying to optimize a 127 | specific component. 128 | 129 | You can specify which processes should be traced using the 130 | input option `scope`: 131 | 132 | [source,erlang] 133 | ---- 134 | 1> lg:trace([{scope, [self()]}, io]). 135 | {call,<0.58.0>,1489494935163831,{io,columns,0}} 136 | {call,<0.58.0>,1489494935163841,{io,default_output,0}} 137 | {return_to,<0.58.0>,1489494935163844,{io,columns,0}} 138 | {call,<0.58.0>,1489494935163846,{io,columns,1}} 139 | ... 140 | ---- 141 | 142 | The list found in the `scope` tuple can take the same values 143 | as the first argument to `erlang:trace/3`. When the tuple is 144 | missing the default will be `processes`. 145 | 146 | The scope tuple can be found multiple time in the input. 147 | This is particularly useful when combining trace definition 148 | callbacks. 149 | 150 | Looking Glass will trace all the processes specified but 151 | also the processes that they create. This means that when 152 | you provide a supervisor pid, all its children will also 153 | be traced, as long as they were started after the start 154 | of the trace session. 155 | 156 | === Trace definition callbacks 157 | 158 | For ease of use, Looking Glass allows you to define functions in 159 | your code that return interesting patterns. This allows you to 160 | define areas of your code that you profile often, or to dynamically 161 | generate the list if necessary. 162 | 163 | To use callbacks, simply provide a callback tuple: 164 | 165 | [source,erlang] 166 | ---- 167 | 1> lg:trace({callback, lg_callgrind, patterns}). 168 | ---- 169 | 170 | You can of course use it in combination with other inputs: 171 | 172 | [source,erlang] 173 | ---- 174 | 1> lg:trace([shell, {callback, lg_callgrind, patterns}]). 175 | ---- 176 | 177 | You can also combine as many callbacks as you wish. 178 | 179 | The callback takes the following form: 180 | 181 | [source,erlang] 182 | ---- 183 | patterns() -> lg:input(). 184 | ---- 185 | 186 | The function name can be anything. A module may have more than one 187 | Looking Glass callback. 188 | 189 | The return value is a list of patterns and scopes that will 190 | be traced. It can therefore contain modules, applications 191 | or other callbacks. 192 | 193 | An example callback could be: 194 | 195 | [source,erlang] 196 | ---- 197 | -module(ranch_lg). 198 | -export([connections/0]). 199 | 200 | %% Trace all events but only from the TCP connection processes. 201 | connections() -> 202 | ConnsPid = ranch_server:get_connections_sup(tcp_echo), 203 | ['_', {scope, [ConnsPid]}]. 204 | ---- 205 | 206 | === Tracers 207 | 208 | Looking Glass comes with a number of tracers. The default is called 209 | `lg_raw_console_tracer` and simply outputs the events to the console, 210 | without any formatting applied. 211 | 212 | The default `lg:trace/1` call is equivalent to the following: 213 | 214 | [source,erlang] 215 | ---- 216 | 1> lg:trace(shell, lg_raw_console_tracer, undefined, #{}). 217 | ---- 218 | 219 | The arguments are, in order, the trace patterns (the modules or 220 | applications that need to be traced), the tracer module, the tracer 221 | options, and the Looking Glass options. 222 | 223 | === Tracing to file 224 | 225 | Looking Glass comes with a tracer that saves all events directly 226 | into a compressed file. Trace files can be used for replaying events 227 | (for example if you're looking for something specific when debugging) 228 | or for profiling. 229 | 230 | Looking Glass compresses the trace files using the LZ4 compression 231 | algorithm. This algorithm was chosen for its very low footprint; 232 | it allows us to reduce the trace file size without putting a strain 233 | on the system being traced. The files produced are compatible with 234 | the LZ4 command line tools. 235 | 236 | The options for this tracer are only the filename: 237 | 238 | [source,erlang] 239 | ---- 240 | 1> lg:trace('_', lg_file_tracer, "traces.lz4"). 241 | ---- 242 | 243 | If you play with the shell a little after running this command, 244 | and then run `lg:stop().` you can see that the following files 245 | have been created: 246 | 247 | [source,bash] 248 | ---- 249 | $ ls -l traces.lz4.* 250 | -rw-r--r-- 1 essen essen 333676 Feb 28 18:24 traces.lz4.1 251 | -rw-r--r-- 1 essen essen 384471 Feb 28 18:24 traces.lz4.2 252 | -rw-r--r-- 1 essen essen 333776 Feb 28 18:24 traces.lz4.3 253 | -rw-r--r-- 1 essen essen 11689 Feb 28 18:24 traces.lz4.4 254 | ---- 255 | 256 | Looking Glass will create one trace file per scheduler by 257 | default (which is typically equal to the number of cores 258 | you have on your machine). The files are split so that 259 | all the events of one process are always stored in the 260 | same file. 261 | 262 | We can use the file reader module coming with Looking Glass 263 | to inspect the contents of the files: 264 | 265 | [source,erlang] 266 | ---- 267 | 2> lg_file_reader:foreach(fun(E) -> erlang:display(E) end, "traces.lz4.1"). 268 | {call,<0.51.0>,1488302656982110,{group,io_request,5}} 269 | {call,<0.51.0>,1488302656982114,{group,io_request,4}} 270 | {call,<0.51.0>,1488302656982117,{group,get_tty_geometry,1}} 271 | {call,<0.75.0>,1488302656982129,{file_io_server,io_request,2}} 272 | ... 273 | ---- 274 | 275 | Careful though, don't run this on production either! 276 | Trace files can become really, really big. 277 | 278 | You may also write a slightly larger fun to filter what 279 | you want to see, for example all events from a single 280 | process: 281 | 282 | [source,erlang] 283 | ---- 284 | 3> Pid = pid(0,51,0). 285 | <0.51.0> 286 | 4> F = fun(E) when element(2, E) =:= Pid -> 287 | erlang:display(E); 288 | (_) -> 289 | ok 290 | end. 291 | #Fun 292 | 5> lg_file_reader:foreach(F, "traces.lz4.1"). 293 | {call,<0.51.0>,1488302656982110,{group,io_request,5}} 294 | {call,<0.51.0>,1488302656982114,{group,io_request,4}} 295 | {call,<0.51.0>,1488302656982117,{group,get_tty_geometry,1}} 296 | {return_to,<0.51.0>,1488302656982306,{group,io_request,4}} 297 | ... 298 | ---- 299 | 300 | === Tracer mode 301 | 302 | When tracing to file for the purposes of profiling, you 303 | most likely do not care about certain events, like processes 304 | being linked. To disable any unnecessary event for profiling, 305 | pass the `mode` option: 306 | 307 | [source,erlang] 308 | ---- 309 | 1> lg:trace('_', lg_file_tracer, "traces.lz4", #{mode => profile}). 310 | ---- 311 | 312 | [[tracing_running]] 313 | You can also get extra events that are only useful for profiling 314 | by enabling options. The `running` option will enable events 315 | indicating when processes are scheduled in or out. It's generally 316 | useful to have as it enables additional stats, but can take a lot 317 | of resources and so isn't enabled by default: 318 | 319 | [source,erlang] 320 | ---- 321 | 1> lg:trace('_', lg_file_tracer, "traces.lz4", 322 | #{mode => profile, running => true}). 323 | ---- 324 | 325 | [[tracing_send]] 326 | You may want to also trace the messages sent by the processes. 327 | To do so you need to enable the `send` option. You can then 328 | xref:messages[obtain detailed information about the processes 329 | sending messages]. To enable the tracing of messages: 330 | 331 | [source,erlang] 332 | ---- 333 | 1> lg:trace('_', lg_file_tracer, "traces.lz4", 334 | #{send => true}). 335 | ---- 336 | 337 | All the options in this section can be combined at will. It is 338 | possible to use the data from the same tracing session when 339 | profiling both functions and messages. 340 | 341 | === Trace file rotation 342 | 343 | For long running sessions Looking Glass can rotate trace files. 344 | This is a feature that helps avoid running out of disk space 345 | and is not meant to be for keeping files small (Looking Glass 346 | can deal with very large files just fine). 347 | 348 | Instead of passing a filename prefix as a third argument to 349 | `lg:trace/3,4`, a map can be provided. There are currently 350 | three options including the `filename_prefix`. The other options 351 | are the maximum file size in bytes, `max_size`, and the number 352 | of events that will be stored per LZ4 frame in the file, 353 | `events_per_frame`. These two options allow you to control 354 | how often the file will be written to or rotated. 355 | 356 | The following example will limit the file sizes to 100MB: 357 | 358 | [source,erlang] 359 | ---- 360 | 1> lg:trace('_', lg_file_tracer, 361 | #{filename_prefix => "traces.lz4", max_size => 100000000}, 362 | #{mode => profile, running => true}). 363 | ---- 364 | 365 | During testing of this feature it appeared that the rotation 366 | as it's currently implemented is expensive, therefore you 367 | should be careful not to put a value that's too low. 368 | -------------------------------------------------------------------------------- /ebin/looking_glass.app: -------------------------------------------------------------------------------- 1 | {application, 'looking_glass', [ 2 | {description, "New project"}, 3 | {vsn, "0.1.0"}, 4 | {modules, ['lg','lg_callgrind','lg_file_reader','lg_file_tracer','lg_flame','lg_messages','lg_messages_seqdiag','lg_rabbit_hole','lg_raw_console_tracer','lg_socket_client','lg_socket_tracer','lg_term','lg_tracer','lg_tracer_pool','looking_glass_app','looking_glass_sup']}, 5 | {registered, [looking_glass_sup]}, 6 | {applications, [kernel,stdlib,runtime_tools,lz4]}, 7 | {mod, {looking_glass_app, []}}, 8 | {env, []} 9 | ]}. -------------------------------------------------------------------------------- /extensions.bzl: -------------------------------------------------------------------------------- 1 | load( 2 | "@bazel_tools//tools/build_defs/repo:git.bzl", 3 | "new_git_repository", 4 | ) 5 | load( 6 | "@bazel_tools//tools/build_defs/repo:utils.bzl", 7 | "maybe", 8 | ) 9 | 10 | NIF_HELPERS_BUILD_FILE_CONTENT = """exports_files([ 11 | "nif_helpers.h", 12 | "nif_helpers.c", 13 | ]) 14 | """ 15 | 16 | def _external_deps(_ctx): 17 | maybe( 18 | repo_rule = new_git_repository, 19 | name = "nif_helpers", 20 | build_file_content = NIF_HELPERS_BUILD_FILE_CONTENT, 21 | commit = "ead6adc15fca3c314351523080d2ccb1956d5956", 22 | remote = "https://github.com/ninenines/nif_helpers", 23 | ) 24 | 25 | external_deps = module_extension( 26 | implementation = _external_deps, 27 | ) 28 | -------------------------------------------------------------------------------- /platforms/BUILD.bazel: -------------------------------------------------------------------------------- 1 | platform( 2 | name = "erlang_internal_platform", 3 | constraint_values = [ 4 | "@erlang_config//:erlang_internal", 5 | ], 6 | parents = ["@rbe//config:platform"], 7 | ) 8 | 9 | platform( 10 | name = "erlang_linux_24_platform", 11 | constraint_values = [ 12 | "@erlang_config//:erlang_24", 13 | ], 14 | parents = ["@rbe//config:platform"], 15 | ) 16 | 17 | platform( 18 | name = "erlang_linux_25_platform", 19 | constraint_values = [ 20 | "@erlang_config//:erlang_25", 21 | ], 22 | parents = ["@rbe//config:platform"], 23 | ) 24 | 25 | platform( 26 | name = "erlang_linux_26_platform", 27 | constraint_values = [ 28 | "@erlang_config//:erlang_26", 29 | ], 30 | parents = ["@rbe//config:platform"], 31 | ) 32 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [ 2 | {lz4, {git, "https://github.com/rabbitmq/lz4-erlang", {branch, "main"}}} 3 | ]}. 4 | 5 | {pre_hooks, [ 6 | {"(linux|darwin|solaris|win32)", clean, "make clean"}, 7 | {"(freebsd|netbsd|openbsd)", clean, "gmake clean"}, 8 | {"(linux|darwin|solaris|win32)", compile, "make"}, 9 | {"(freebsd|netbsd|openbsd)", compile, "gmake"} 10 | ]}. 11 | -------------------------------------------------------------------------------- /src/lg.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg). 16 | 17 | -export([trace/1]). 18 | -export([trace/2]). 19 | -export([trace/3]). 20 | -export([trace/4]). 21 | -export([stop/0]). 22 | -export([stop/1]). 23 | 24 | -type pattern() :: module() | {app, atom()} | {callback, module(), atom()}. 25 | -type scope() :: {scope, [ 26 | pid() | port() | all | processes | ports | 27 | existing | existing_processes | existing_ports | 28 | new | new_processes | new_ports]}. 29 | 30 | -type input() :: [pattern() | scope()]. 31 | -export_type([input/0]). 32 | 33 | %% The trace functions input is not as strict for user convenience. 34 | -type user_input() :: pattern() | input(). 35 | 36 | -type opts() :: #{ 37 | mode => trace | profile, 38 | pool_id => any(), 39 | pool_size => pos_integer(), 40 | send => boolean(), 41 | running => boolean() 42 | }. 43 | 44 | -spec trace(user_input()) -> ok. 45 | trace(Input) -> 46 | trace(Input, lg_raw_console_tracer). 47 | 48 | -spec trace(user_input(), module()) -> ok. 49 | trace(Input, TracerMod) -> 50 | trace(Input, TracerMod, undefined, #{}). 51 | 52 | -spec trace(user_input(), module(), any()) -> ok. 53 | trace(Input, TracerMod, TracerOpts) -> 54 | trace(Input, TracerMod, TracerOpts, #{}). 55 | 56 | -spec trace(user_input(), module(), any(), opts()) -> ok. 57 | trace(Input, TracerMod, TracerOpts, Opts) when is_list(Input) -> 58 | do_trace(Input, TracerMod, TracerOpts, Opts); 59 | trace(Input, TracerMod, TracerOpts, Opts) -> 60 | trace([Input], TracerMod, TracerOpts, Opts). 61 | 62 | do_trace(Input0, TracerMod, TracerOpts, Opts) -> 63 | %% @todo Remove eventually? 64 | _ = application:ensure_all_started(looking_glass), 65 | %% Start the pool of tracer processes. 66 | PoolID = maps:get(pool_id, Opts, default), 67 | PoolSize = maps:get(pool_size, Opts, erlang:system_info(schedulers)), 68 | true = PoolSize > 0, 69 | {ok, PoolPid} = supervisor:start_child(looking_glass_sup, #{ 70 | id => PoolID, 71 | start => {lg_tracer_pool, start_link, [PoolSize, TracerMod, TracerOpts]}, 72 | restart => temporary, 73 | type => supervisor 74 | }), 75 | Tracers = lg_tracer_pool:tracers(PoolPid), 76 | TracersMap = maps:from_list(lists:zip(lists:seq(0, length(Tracers) - 1), Tracers)), 77 | Mode = maps:get(mode, Opts, trace), 78 | Input1 = flatten(Input0, []), 79 | Input2 = ensure_pattern(Input1), 80 | Input = ensure_scope(Input2), 81 | trace_input(Input, #{mode => Mode, tracers => TracersMap}, Opts), 82 | ok. 83 | 84 | flatten([], Acc) -> 85 | lists:flatten(Acc); 86 | flatten([{callback, Mod, Fun}|Tail], Acc) when is_atom(Mod), is_atom(Fun) -> 87 | Input = flatten(Mod:Fun(), []), 88 | flatten(Tail, [Input|Acc]); 89 | flatten([{app, App}|Tail], Acc) when is_atom(App) -> 90 | _ = application:load(App), 91 | {ok, Mods} = application:get_key(App, modules), 92 | flatten(Tail, [Mods|Acc]); 93 | flatten([Input|Tail], Acc) -> 94 | flatten(Tail, [Input|Acc]). 95 | 96 | ensure_pattern(Input) -> 97 | case [S || S={scope, _} <- Input] of 98 | Input -> ['_'|Input]; 99 | _ -> Input 100 | end. 101 | 102 | ensure_scope(Input) -> 103 | case [S || S={scope, _} <- Input] of 104 | [] -> [{scope, [processes]}|Input]; 105 | _ -> Input 106 | end. 107 | 108 | trace_input([], _, _) -> 109 | ok; 110 | trace_input([{scope, Scope}|Tail], TracerState, Opts) -> 111 | %% We currently enable the following trace flags: 112 | %% - call: function calls 113 | %% - procs: process exit events; plus others we ignore 114 | %% - running: process being scheduled in/out 115 | %% - timestamp: events include the current timestamp 116 | %% - arity: function calls only include the arity, not arguments 117 | %% - return_to: return from functions 118 | %% - set_on_spawn: propagate trace flags to any children processes 119 | %% 120 | %% @todo It might be useful to count the number of sends 121 | %% or receives a function does. 122 | ExtraFlags = [running || maps:get(running, Opts, false)] 123 | ++ [send || maps:get(send, Opts, false)], 124 | _ = [erlang:trace(PidPortSpec, true, [ 125 | call, procs, timestamp, arity, return_to, set_on_spawn, 126 | {tracer, lg_tracer, TracerState} 127 | |ExtraFlags 128 | ]) 129 | || PidPortSpec <- Scope], 130 | trace_input(Tail, TracerState, Opts); 131 | trace_input([Mod|Tail], TracerState, Opts) when is_atom(Mod) -> 132 | MatchSpec = case Opts of 133 | #{process_dump := true} -> [{'_', [], [{message, {process_dump}}]}]; 134 | _ -> true 135 | end, 136 | %% The module must be loaded before we attempt to trace it. 137 | _ = code:ensure_loaded(Mod), 138 | _ = erlang:trace_pattern({Mod, '_', '_'}, MatchSpec, [local]), 139 | trace_input(Tail, TracerState, Opts). 140 | 141 | stop() -> 142 | stop(default). 143 | 144 | %% @todo Confirm that we don't need to stop tracing, 145 | %% that just terminating the tracers is enough. The 146 | %% NIF does cancel traces when tracers go away, but 147 | %% better make sure. 148 | stop(PoolID) -> 149 | supervisor:terminate_child(looking_glass_sup, PoolID). 150 | -------------------------------------------------------------------------------- /src/lg_callgrind.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_callgrind). 16 | 17 | -export([patterns/0]). 18 | -export([profile/2]). 19 | -export([profile/3]). 20 | -export([profile_many/2]). 21 | -export([profile_many/3]). 22 | 23 | %% @todo Add an option with a list of modules to exclude. 24 | 25 | -type opts() :: #{ 26 | %% Whether we filter the output per process. 27 | scope => global | per_process, 28 | %% Whether we compute and save wait times. 29 | running => boolean() 30 | }. 31 | 32 | -record(call, { 33 | %% The MFA for the call. 34 | mfa :: atom(), 35 | %% The source file name. 36 | source :: {string(), pos_integer()}, 37 | %% The timestamp for the call. 38 | ts :: pos_integer(), 39 | %% The timestamp for when we last started executing this function. 40 | self_ts :: pos_integer(), 41 | %% Execution time including subcalls. 42 | incl :: undefined | non_neg_integer(), 43 | %% Execution time excluding subcalls. 44 | self = 0 :: integer(), 45 | %% Number of times the function was called. 46 | count = 1 :: pos_integer(), 47 | %% Time when the process was not running in this function. 48 | wait = 0 :: non_neg_integer(), 49 | %% Time when the process was not running in this function or any subcalls. 50 | wait_incl = 0 :: non_neg_integer(), 51 | %% Number of times the process was scheduled out. 52 | wait_count = 0 :: non_neg_integer(), 53 | %% Number of times the function or any subcall was scheduled out. 54 | wait_count_incl = 0 :: non_neg_integer(), 55 | %% Calls done by this MFA. 56 | calls = #{} :: #{atom() => #call{}} 57 | }). 58 | 59 | -record(proc, { 60 | %% Call stack. 61 | stack = [] :: [#call{}], 62 | %% Profile information waiting to be written to file. 63 | mfas = #{} :: #{atom() => #call{}}, 64 | %% Timestamp the process got scheduled out. 65 | out = undefined :: undefined | non_neg_integer() 66 | }). 67 | 68 | -record(state, { 69 | %% Input file name. 70 | input :: file:filename_all(), 71 | %% Output file name. 72 | output :: file:filename_all(), 73 | %% Output fd. 74 | output_device :: file:io_device(), 75 | %% Options. 76 | opts :: opts(), 77 | %% List of processes. 78 | processes = #{} :: #{pid() => #proc{}}, 79 | %% Cache of source file information. 80 | sources = #{} :: #{mfa() => {string(), pos_integer()}} 81 | }). 82 | 83 | -spec patterns() -> lg:input(). 84 | patterns() -> 85 | [{app, kernel}, {app, stdlib}, {app, looking_glass}]. 86 | 87 | -spec profile(file:filename_all(), file:filename_all()) -> ok. 88 | profile(Input, Output) -> 89 | profile(Input, Output, #{}). 90 | 91 | -spec profile(file:filename_all(), file:filename_all(), opts()) -> ok. 92 | profile(Input, Output, Opts) -> 93 | {ok, OutDevice} = file:open(Output, [write]), 94 | State = #state{input=Input, output=Output, output_device=OutDevice, opts=Opts}, 95 | write_header(State), 96 | {ok, FinalState} = lg_file_reader:fold(fun handle_event/2, State, Input), 97 | flush(FinalState), 98 | _ = file:close(OutDevice), 99 | ok. 100 | 101 | flush(State=#state{processes=Procs}) -> 102 | maps:fold(fun(Pid, #proc{mfas=MFAs}, _) -> 103 | write_mfas(Pid, MFAs, State) 104 | end, undefined, Procs), 105 | ok. 106 | 107 | -spec profile_many(file:filename(), file:filename()) -> ok. 108 | profile_many(Wildcard, Prefix) -> 109 | profile_many(Wildcard, Prefix, #{}). 110 | 111 | -spec profile_many(file:filename(), file:filename(), opts()) -> ok. 112 | profile_many(Wildcard, Prefix, Opts) -> 113 | Files = filelib:wildcard(Wildcard), 114 | Seq = lists:seq(1, length(Files)), 115 | OutFiles = [Prefix ++ "." ++ integer_to_list(N) || N <- Seq], 116 | Many = lists:zip(Files, OutFiles), 117 | Refs = [monitor(process, spawn_link(?MODULE, profile, [Input, Output, Opts])) 118 | || {Input, Output} <- Many], 119 | wait_for_procs(Refs). 120 | 121 | %% We do not need to worry about failure because we are linked. 122 | wait_for_procs([]) -> 123 | ok; 124 | wait_for_procs(Refs) -> 125 | receive 126 | %% We purposefully ignore any stray messages. 127 | {'DOWN', R, process, _, _} -> 128 | wait_for_procs(Refs -- [R]) 129 | end. 130 | 131 | %% We handle trace events one by one, keeping track of the 132 | %% execution stack for each process. 133 | 134 | %% We don't care about match spec results for callgrind. 135 | handle_event({call, Pid, Ts, MFA, _MSpec}, State) -> 136 | handle_event({call, Pid, Ts, MFA}, State); 137 | handle_event({call, Pid, Ts, MFA}, State0) -> 138 | Proc = case is_process_profiled(Pid, State0) of 139 | {true, P} -> P; 140 | {empty, P} -> P; 141 | false -> #proc{} 142 | end, 143 | {Source, State} = find_source(MFA, State0), 144 | handle_call(Pid, convert_mfa(MFA), Source, Ts, Proc, State); 145 | handle_event({return_to, Pid, Ts, MFA}, State) -> 146 | case is_process_profiled(Pid, State) of 147 | {true, Proc} -> handle_return_to(Pid, convert_mfa(MFA), Ts, Proc, State); 148 | _ -> State 149 | end; 150 | %% Process exited. Unfold the stacktrace entirely. 151 | %% 152 | %% We use the atom exit because we know it will not match 153 | %% a function call and will therefore unfold everything. 154 | handle_event({exit, Pid, Ts, _Reason}, State0) -> 155 | case is_process_profiled(Pid, State0) of 156 | {true, Proc} -> 157 | State=#state{processes=Procs} = handle_return_to(Pid, exit, Ts, Proc, State0), 158 | %% Remove the pid from the state to save memory. 159 | State#state{processes=maps:remove(Pid, Procs)}; 160 | _ -> 161 | State0 162 | end; 163 | handle_event({in, Pid, Ts, _MFA}, State=#state{opts=#{running := true}}) -> 164 | case is_process_profiled(Pid, State) of 165 | {true, Proc} -> handle_in(Pid, Ts, Proc, State); 166 | _ -> State 167 | end; 168 | handle_event({out, Pid, Ts, _MFA}, State=#state{opts=#{running := true}}) -> 169 | case is_process_profiled(Pid, State) of 170 | {true, Proc} -> handle_out(Pid, Ts, Proc, State); 171 | _ -> State 172 | end; 173 | %% Ignore all other events. We do not need them for building the callgrind file. 174 | handle_event(_, State) -> 175 | State. 176 | 177 | is_process_profiled(Pid, #state{processes=Procs}) -> 178 | case maps:get(Pid, Procs, undefined) of 179 | %% We never received events for this process. Ignore. 180 | undefined -> false; 181 | %% We received events but are not in a known function currently. Ignore. 182 | Proc=#proc{stack=[]} -> {empty, Proc}; 183 | %% All good! 184 | Proc -> {true, Proc} 185 | end. 186 | 187 | %% We track a number of different things: 188 | %% - how much time was spent in the different function calls 189 | %% - how much time they spent calling other functions 190 | %% - how many times functions were called 191 | %% 192 | %% We track everything on a per process basis. For each process, 193 | %% we maintain a call stack. Every time a function return, we may 194 | %% end up saving call information to the 'mfas' map. We then write 195 | %% this information to the disk whenever the stacktrace becomes 196 | %% empty, or when the process terminates. 197 | 198 | %% When we receive a call event, we add the call information 199 | %% to the stack, regardless of what it already contains. 200 | %% This means that recursive calls, whether tail or body, 201 | %% will appear multiple times in the stack. And since Erlang 202 | %% doesn't have loops, it will appear a little weird if 203 | %% compared to an imperative language. 204 | 205 | %% Recursive call. Just increase the call count. 206 | handle_call(Pid, MFA, _Source, _Ts, 207 | Proc0=#proc{stack=[Call=#call{mfa=MFA, count=Count}|Stack0]}, 208 | State=#state{processes=Procs}) -> 209 | Stack = [Call#call{count=Count + 1}|Stack0], 210 | Proc = Proc0#proc{stack=Stack}, 211 | State#state{processes=Procs#{Pid => Proc}}; 212 | %% Non-recursive call. 213 | handle_call(Pid, MFA, Source, Ts, Proc0=#proc{stack=Stack0}, 214 | State=#state{processes=Procs}) -> 215 | Stack = [#call{mfa=MFA, source=Source, ts=Ts, self_ts=Ts}|Stack0], 216 | Proc = Proc0#proc{stack=Stack}, 217 | State#state{processes=Procs#{Pid => Proc}}. 218 | 219 | %% We return from the current call, so the current call 220 | %% ends regardless of what it was doing. We stop as soon 221 | %% as we see the caller we return to; or if we return all 222 | %% the way up, higher than where we started (for example 223 | %% because we were not tracing the function we actually 224 | %% end up returning to), we get everything. 225 | %% 226 | %% The current call started when it was called and stopped 227 | %% on the return_to timestamp. Therefore it is fairly simple 228 | %% to calculate its incl/self times. 229 | %% 230 | %% Other calls returning at the same time are tail calls. 231 | %% In their case, the incl time is the same as for the 232 | %% current call. However the self time must not stop when 233 | %% returning but rather when doing the final tail call. 234 | %% We also update sub call times since those must be 235 | %% maintained separately. 236 | %% 237 | %% NOTE: Due to how the VM works, if a function has both 238 | %% tail and non-tail calls, it becomes impossible to know 239 | %% what is or is not a tail call, and therefore values 240 | %% may be wrong. Do not write such functions! For example: 241 | %% 242 | %% a(true) -> 1 + b(); a(false) -> b(). 243 | %% 244 | %% Finally we must also update the self for the call we 245 | %% actually return to. In its case we use the time we 246 | %% were last executing the function as a start point, 247 | %% and the return time for the end. Here again we also 248 | %% update the sub call times. 249 | 250 | handle_return_to(Pid, MFA, Ts, Proc0=#proc{stack=[Current0|Stack0], mfas=MFAs0}, 251 | State=#state{processes=Procs}) -> 252 | {Returned0, Stack1} = lists:splitwith( 253 | fun(#call{mfa=E}) -> E =/= MFA end, 254 | Stack0), 255 | #call{ts=CurrentTs, self_ts=CurrentSelfTs, self=CurrentSelf} = Current0, 256 | Current = Current0#call{incl=Ts - CurrentTs, self=CurrentSelf + Ts - CurrentSelfTs}, 257 | Returned = update_tail_calls([Current|Returned0], Ts), 258 | Stack = update_stack(Returned, Stack1, Ts), 259 | %% Save the profile information in the state, potentially flushing it 260 | %% to disk if the stack is empty. 261 | MFAs1 = update_mfas(Returned, MFAs0), 262 | MFAs = case Stack of 263 | [] -> 264 | write_mfas(Pid, MFAs1, State), 265 | #{}; 266 | _ -> 267 | MFAs1 268 | end, 269 | Proc = Proc0#proc{stack=Stack, mfas=MFAs}, 270 | State#state{processes=Procs#{Pid => Proc}}. 271 | 272 | update_tail_calls([Call], _) -> 273 | [Call]; 274 | update_tail_calls([ 275 | Callee=#call{ts=CalleeTs}, 276 | Caller0=#call{ts=CallerTs, self_ts=CallerSelfTs, self=CallerSelf} 277 | |Tail], ReturnToTs) -> 278 | Caller1 = Caller0#call{ 279 | incl=ReturnToTs - CallerTs, 280 | self=CallerSelf + CalleeTs - CallerSelfTs 281 | }, 282 | Caller = update_sub_calls(Callee, Caller1), 283 | [Callee|update_tail_calls([Caller|Tail], ReturnToTs)]. 284 | 285 | %% Update nothing; there's nothing in the stack. 286 | update_stack(_, [], _) -> 287 | []; 288 | %% Update the incl/self value based on the top-level function we return from, 289 | %% but only update the sub calls based on the function we directly called. 290 | update_stack(Returned, 291 | [Caller0=#call{self_ts=CallerSelfTs, self=CallerSelf}|Stack], 292 | ReturnToTs) -> 293 | Callee = #call{ts=CalleeTs} = hd(lists:reverse(Returned)), 294 | Caller = Caller0#call{ 295 | self_ts=ReturnToTs, 296 | self=CallerSelf + CalleeTs - CallerSelfTs 297 | }, 298 | [update_sub_calls(Callee, Caller)|Stack]. 299 | 300 | update_sub_calls(Callee=#call{mfa=MFA, incl=CallerIncl, count=CallerCount, 301 | wait_incl=CallerWaitIncl}, Caller=#call{calls=SubCalls}) -> 302 | case maps:get(MFA, SubCalls, undefined) of 303 | %% Add the callee to the subcalls but remove the callee's subcalls 304 | %% since we don't need those here. 305 | undefined -> 306 | Caller#call{calls=SubCalls#{MFA => Callee#call{calls=#{}}}}; 307 | %% Same as above, except we add to the existing values. 308 | Sub = #call{incl=SubIncl, count=SubCount, wait_incl=SubWaitIncl} -> 309 | Caller#call{calls=SubCalls#{MFA => Sub#call{ 310 | %% We do not care about self/wait here as we will be using incl/wait_incl in the output. 311 | incl=SubIncl + CallerIncl, 312 | count=SubCount + CallerCount, 313 | wait_incl=SubWaitIncl + CallerWaitIncl 314 | }}} 315 | end. 316 | 317 | %% Processes get scheduled in and out. We get the corresponding 318 | %% in and out events when the 'running' option is set to true. 319 | %% We keep track of how many times the process was scheduled out 320 | %% per function, and how long. 321 | 322 | handle_in(Pid, InTs, Proc0=#proc{stack=[Current0|Stack0], out=OutTs}, 323 | State=#state{processes=Procs}) -> 324 | #call{wait=Wait, wait_incl=WaitIncl, 325 | wait_count=WaitCount, wait_count_incl=WaitCountIncl 326 | } = Current0, 327 | ThisWait = InTs - OutTs, 328 | %% We increase the wait time for self first. 329 | Current = Current0#call{wait=Wait + ThisWait, wait_incl=WaitIncl + ThisWait, 330 | wait_count=WaitCount + 1, wait_count_incl=WaitCountIncl + 1}, 331 | %% And then for the parent calls to include wait time of subcalls. 332 | Stack = [ 333 | Call#call{wait_incl=ParentWaitIncl + ThisWait, wait_count_incl=ParentWaitCount + 1} 334 | || Call=#call{wait_incl=ParentWaitIncl, wait_count_incl=ParentWaitCount} <- Stack0], 335 | Proc = Proc0#proc{stack=[Current|Stack], out=undefined}, 336 | State#state{processes=Procs#{Pid => Proc}}. 337 | 338 | handle_out(Pid, Ts, Proc0=#proc{out=undefined}, 339 | State=#state{processes=Procs}) -> 340 | Proc = Proc0#proc{out=Ts}, 341 | State#state{processes=Procs#{Pid => Proc}}. 342 | 343 | %% Update the profiling information we currently hold. 344 | update_mfas([], MFAs) -> 345 | MFAs; 346 | update_mfas([Call=#call{mfa=MFA, incl=Incl, self=Self, wait=Wait, wait_incl=WaitIncl, 347 | wait_count=WaitCount, wait_count_incl=WaitCountIncl, 348 | count=Count, calls=SubCalls}|Tail], MFAs) -> 349 | case MFAs of 350 | #{MFA := AggCall0=#call{incl=AggIncl, self=AggSelf, wait=AggWait, wait_incl=AggWaitIncl, 351 | wait_count=AggWaitCount, wait_count_incl=AggWaitCountIncl, 352 | count=AggCount, calls=AggSubCalls0}} -> 353 | AggSubCalls = update_mfas(maps:values(SubCalls), AggSubCalls0), 354 | AggCall=AggCall0#call{incl=Incl + AggIncl, self=Self + AggSelf, 355 | wait=Wait + AggWait, wait_incl=WaitIncl + AggWaitIncl, 356 | wait_count=WaitCount + AggWaitCount, 357 | wait_count_incl=WaitCountIncl + AggWaitCountIncl, 358 | count=Count + AggCount, calls=AggSubCalls}, 359 | update_mfas(Tail, MFAs#{MFA => AggCall}); 360 | _ -> 361 | update_mfas(Tail, MFAs#{MFA => Call}) 362 | end. 363 | 364 | %% The callgrind format is documented at http://valgrind.org/docs/manual/cl-format.html 365 | %% 366 | %% We currently only store the real time spent in the calls 367 | %% (including wait times). 368 | %% 369 | %% The option 'scope' can be used to enable per process tracking. 370 | 371 | write_header(#state{output_device=OutDevice, opts=#{running := true}}) -> 372 | ok = file:write(OutDevice, 373 | "# callgrind format\n" 374 | "events: Total Active Wait WaitCount\n" 375 | "event: Total : Total time in microseconds\n" 376 | "event: Active : Active time in microseconds\n" 377 | "event: Wait : Wait time in microseconds (scheduled out)\n" 378 | "event: WaitCount : Number of times the process was scheduled out\n" 379 | "\n"); 380 | write_header(#state{output_device=OutDevice}) -> 381 | ok = file:write(OutDevice, 382 | "# callgrind format\n" 383 | "events: Total\n" 384 | "event: Total : Total time in microseconds\n" 385 | "\n"). 386 | 387 | write_mfas(Pid, MFAs, State) -> 388 | _ = [write_call(Pid, Call, State) || Call <- maps:values(MFAs)], 389 | ok. 390 | 391 | write_call(Pid, #call{mfa=MFA, source={Source, LN0}, self=Self, wait=Wait, 392 | wait_count=WaitCount, calls=Calls0}, 393 | #state{output_device=OutDevice, opts=Opts}) -> 394 | LN = line_number(LN0), 395 | Calls = maps:values(Calls0), 396 | Ob = case Opts of 397 | #{scope := per_process} -> 398 | ["ob=", io_lib:write(Pid), "\n"]; 399 | _ -> 400 | [] 401 | end, 402 | RunningCosts = case Opts of 403 | #{running := true} -> 404 | [ 405 | " ", integer_to_list(Self - Wait), 406 | " ", integer_to_list(Wait), 407 | " ", integer_to_list(WaitCount) 408 | ]; 409 | _ -> 410 | [] 411 | end, 412 | file:write(OutDevice, [Ob, 413 | "fl=", Source, "\n" 414 | "fn=", atom_to_list(MFA), "\n", 415 | integer_to_list(LN), " ", integer_to_list(Self), RunningCosts, "\n", 416 | format_subcalls(LN, Calls, Opts), 417 | "\n"]). 418 | 419 | format_subcalls(_, [], _) -> 420 | []; 421 | %% @todo We don't need to write the filename for functions in the same module. 422 | %% @todo We also don't want to put the full file name; instead we should remove 423 | %% the prefix (path to the release). 424 | %% 425 | %% We only look at where the function is defined, we can't really get 426 | %% the actual line number where the call happened, unfortunately. 427 | format_subcalls(LN, [#call{mfa=MFA, source={Source, TargetLN0}, incl=Incl, 428 | wait_incl=Wait, wait_count_incl=WaitCount, count=Count, calls=_Calls}|Tail], Opts) -> 429 | TargetLN = line_number(TargetLN0), 430 | RunningCosts = case Opts of 431 | #{running := true} -> 432 | [ 433 | " ", integer_to_list(Incl - Wait), 434 | " ", integer_to_list(Wait), 435 | " ", integer_to_list(WaitCount) 436 | ]; 437 | _ -> 438 | [] 439 | end, 440 | [[ 441 | "cfi=", Source, "\n" 442 | "cfn=", atom_to_list(MFA), "\n" 443 | "calls=", integer_to_list(Count), " ", integer_to_list(TargetLN), "\n", 444 | integer_to_list(LN), " ", integer_to_list(Incl), RunningCosts, "\n" 445 | ]|format_subcalls(LN, Tail, Opts)]. 446 | 447 | %% Starting from OTP-24 we now have {Line, Column}. 448 | -dialyzer({nowarn_function, line_number/1}). 449 | line_number({LN, _}) -> LN; 450 | line_number(LN) -> LN. 451 | 452 | convert_mfa(undefined) -> 453 | undefined; 454 | convert_mfa({M0, F0, A0}) -> 455 | M = atom_to_binary(M0, latin1), 456 | F = atom_to_binary(F0, latin1), 457 | A = integer_to_binary(A0), 458 | binary_to_atom(<>, latin1). 459 | 460 | find_source(MFA, State0=#state{sources=Cache}) -> 461 | case Cache of 462 | #{MFA := Source} -> 463 | {Source, State0}; 464 | _ -> 465 | State = #state{sources=#{MFA := Source}} = cache_module(MFA, State0), 466 | {Source, State} 467 | end. 468 | 469 | %% We extract the line number of the functions by loading the 470 | %% beam file (which is already loaded when we reach this function) 471 | %% and looking into the abstract code directly. When something 472 | %% goes wrong, for example the module was not compiled with 473 | %% +debug_info, the function will return line number 1. 474 | %% 475 | %% Note that we can only retrieve the location of the function. 476 | %% For functions with many clauses we are not able to properly 477 | %% identify which clause was involved. It's probably a good 478 | %% idea to structure your code to have more functions than 479 | %% function clauses, especially when using behaviours. 480 | %% 481 | %% While this is an expensive operation, we cache the result 482 | %% and therefore this function will only be called once per module. 483 | cache_module(MFA={Module, _, _}, State0=#state{sources=Cache}) -> 484 | try 485 | %% If the module is in the path, we can simply query 486 | %% it for the source file. 487 | Info = Module:module_info(compile), 488 | %% @todo We don't want to return an absolute path, 489 | %% but rather the app/src/file.erl path if it's in 490 | %% an application, or just folder/file.erl if not. 491 | %% This allows different users to point to the 492 | %% same source at different locations on their machine. 493 | {_, Src} = lists:keyfind(source, 1, Info), 494 | cache_module(MFA, State0, Src) 495 | catch _:_ -> 496 | %% Either the module was not found, or it doesn't 497 | %% have a 'source' key in the compile info. 498 | %% 499 | %% We can't cache the module; on the other hand 500 | %% we can cache the result of this operation. 501 | %% Just append .erl to the module name and set the 502 | %% line number to 1, which is of course incorrect. 503 | State0#state{sources=Cache#{MFA => {atom_to_list(Module) ++ ".erl", 1}}} 504 | end. 505 | 506 | cache_module(MFA={Module, _, _}, State=#state{sources=Cache0}, Src) -> 507 | {Module, Beam, _} = code:get_object_code(Module), 508 | {ok, {Module, Chunks}} = beam_lib:chunks(Beam, [abstract_code]), 509 | [{abstract_code, {raw_abstract_v1, Forms}}] = Chunks, 510 | Funcs = [{{Module, F, A}, LN} || {function, LN, F, A, _} <- Forms], 511 | Cache1 = lists:foldl(fun({Key, LN}, Acc) -> Acc#{Key => {Src, LN}} end, Cache0, Funcs), 512 | %% We cannot currently retrieve line number information 513 | %% for list comprehensions and funs. We therefore 514 | %% cache the correct file with line number set to 1. 515 | Cache = case Cache1 of 516 | #{MFA := _} -> Cache1; 517 | _ -> Cache1#{MFA => {Src, 1}} 518 | end, 519 | State#state{sources=Cache}. 520 | -------------------------------------------------------------------------------- /src/lg_file_reader.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_file_reader). 16 | 17 | -export([fold/3]). 18 | -export([foreach/2]). 19 | 20 | -export([open/1]). 21 | -export([read_event/1]). 22 | -export([close/1]). 23 | 24 | -record(state, { 25 | io_device :: file:io_device(), 26 | ctx :: lz4f:dctx(), 27 | buffer = <<>> :: binary(), 28 | offset = 0 :: non_neg_integer(), 29 | uncompressed_offset = 0 :: non_neg_integer() 30 | }). 31 | 32 | %% High level API. 33 | 34 | fold(Fun, Acc, Filename) -> 35 | {ok, IoDevice} = open(Filename), 36 | Ctx = lz4f:create_decompression_context(), 37 | Ret = fold1(#state{io_device=IoDevice, ctx=Ctx}, Fun, Acc), 38 | ok = close(IoDevice), 39 | Ret. 40 | 41 | fold1(State0, Fun, Acc0) -> 42 | case read_event(State0) of 43 | {ok, Event, State} -> 44 | Acc = Fun(Event, Acc0), 45 | fold1(State, Fun, Acc); 46 | eof -> 47 | {ok, Acc0}; 48 | Error = {error, _, _} -> 49 | Error 50 | end. 51 | 52 | foreach(Fun, Filename) -> 53 | {ok, IoDevice} = open(Filename), 54 | Ctx = lz4f:create_decompression_context(), 55 | Ret = foreach1(#state{io_device=IoDevice, ctx=Ctx}, Fun), 56 | ok = close(IoDevice), 57 | Ret. 58 | 59 | foreach1(State0, Fun) -> 60 | case read_event(State0) of 61 | {ok, Event, State} -> 62 | _ = Fun(Event), 63 | foreach1(State, Fun); 64 | eof -> 65 | ok; 66 | Error = {error, _, _} -> 67 | Error 68 | end. 69 | 70 | %% Low level API. 71 | 72 | open(Filename) -> 73 | file:open(Filename, [read, binary]). 74 | 75 | read_event(State=#state{buffer=Buffer}) -> 76 | case Buffer of 77 | <> -> 78 | convert_event_body(State#state{buffer=Rest}, Bin); 79 | _ -> 80 | read_file(State) 81 | end. 82 | 83 | read_file(State=#state{io_device=IoDevice, ctx=Ctx, buffer=Buffer, offset=Offset}) -> 84 | case file:read(IoDevice, 1000) of 85 | {ok, Data0} -> 86 | Data = iolist_to_binary(lz4f:decompress(Ctx, Data0)), 87 | read_event(State#state{buffer= <>, 88 | offset=Offset + byte_size(Data0)}); 89 | eof -> 90 | eof; 91 | {error, Reason} -> 92 | {error, Reason, 93 | 'An error occurred while trying to read from the file.'} 94 | end. 95 | 96 | convert_event_body(State=#state{offset=Offset, uncompressed_offset=UnOffset}, Bin) -> 97 | try binary_to_term(Bin) of 98 | Term -> 99 | {ok, Term, State#state{uncompressed_offset=UnOffset + byte_size(Bin)}} 100 | catch Class:Reason -> 101 | {error, {crash, Class, Reason, Offset, UnOffset}, 102 | 'The binary form of an event could not be decoded to an Erlang term.'} 103 | end. 104 | 105 | close(IoDevice) -> 106 | file:close(IoDevice). 107 | -------------------------------------------------------------------------------- /src/lg_file_tracer.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_file_tracer). 16 | 17 | -export([start_link/2]). 18 | -export([init/3]). 19 | 20 | -export([system_continue/3]). 21 | -export([system_terminate/4]). 22 | -export([system_code_change/4]). 23 | 24 | -record(state, { 25 | parent :: pid(), 26 | filename :: file:filename_all(), 27 | size = 0 :: non_neg_integer(), 28 | max_size :: infinity | non_neg_integer(), 29 | io_device :: file:io_device(), 30 | events_per_frame :: pos_integer(), 31 | events_this_frame = 0 :: non_neg_integer(), 32 | buffer = <<>> :: binary() 33 | }). 34 | 35 | start_link(Nth, Prefix) when is_list(Prefix) -> 36 | start_link(Nth, #{filename_prefix => Prefix}); 37 | start_link(Nth, Opts) when is_map(Opts) -> 38 | Pid = proc_lib:spawn_link(?MODULE, init, [self(), Nth, Opts]), 39 | {ok, Pid}. 40 | 41 | init(Parent, Nth, Opts) -> 42 | %% Store all messages off the heap to avoid unnecessary GC. 43 | process_flag(message_queue_data, off_heap), 44 | %% We need to trap exit signals in order to shutdown properly. 45 | process_flag(trap_exit, true), 46 | %% No need to close the file, it'll be closed when the process exits. 47 | Filename = filename:flatten([ 48 | maps:get(filename_prefix, Opts, "traces.lz4"), 49 | ".", integer_to_list(Nth)]), 50 | {ok, IoDevice} = file:open(Filename, [write, raw]), 51 | loop(#state{parent=Parent, filename=Filename, io_device=IoDevice, 52 | max_size=maps:get(max_size, Opts, infinity), 53 | events_per_frame=maps:get(events_per_frame, Opts, 100000)}). 54 | 55 | loop(State=#state{parent=Parent, size=Size, io_device=IoDevice, 56 | events_per_frame=MaxEvents, events_this_frame=NumEvents0, buffer=Buffer0}) -> 57 | receive 58 | {'EXIT', Parent, Reason} -> 59 | terminate(Reason, State); 60 | {system, From, Request} -> 61 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State); 62 | Msg0 -> 63 | Msg = lg_term:truncate(Msg0), 64 | Bin = term_to_binary(Msg), 65 | BinSize = byte_size(Bin), 66 | Buffer = <>, 67 | NumEvents = NumEvents0 + 1, 68 | if 69 | MaxEvents =:= NumEvents -> 70 | Frame = lz4f:compress_frame(Buffer), 71 | ok = file:write(IoDevice, Frame), 72 | maybe_rotate(State#state{size=Size + byte_size(Frame), 73 | events_this_frame=0, buffer= <<>>}); 74 | true -> 75 | loop(State#state{events_this_frame=NumEvents, buffer=Buffer}) 76 | end 77 | end. 78 | 79 | maybe_rotate(State=#state{filename=Filename, size=Size, max_size=MaxSize, 80 | io_device=OldIoDevice}) when Size > MaxSize -> 81 | ok = file:close(OldIoDevice), 82 | ok = file:rename(Filename, Filename ++ ".bak"), 83 | {ok, NewIoDevice} = file:open(Filename, [write, raw]), 84 | loop(State#state{size=0, io_device=NewIoDevice}); 85 | maybe_rotate(State) -> 86 | loop(State). 87 | 88 | system_continue(_, _, State) -> 89 | loop(State). 90 | 91 | -spec system_terminate(any(), _, _, #state{}) -> no_return(). 92 | system_terminate(Reason, _, _, State) -> 93 | terminate(Reason, State). 94 | 95 | system_code_change(Misc, _, _, _) -> 96 | {ok, Misc}. 97 | 98 | -spec terminate(any(), #state{}) -> no_return(). 99 | terminate(Reason, #state{io_device=IoDevice, buffer=Buffer}) -> 100 | _ = file:write(IoDevice, lz4f:compress_frame(Buffer)), 101 | exit(Reason). 102 | -------------------------------------------------------------------------------- /src/lg_flame.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_flame). 16 | 17 | -export([profile/2]). 18 | -export([profile_many/2]). 19 | 20 | -record(state, { 21 | output_path="", 22 | pid, 23 | last_ts, 24 | count=0, 25 | acc=[]}). % per-process state 26 | 27 | -spec profile(file:filename_all(), file:filename_all()) -> ok. 28 | profile(Input, Output) -> 29 | InitialState = exp1_init(Output), 30 | {ok, FinalState} = lg_file_reader:fold(fun handle_event/2, InitialState, Input), 31 | flush(FinalState). 32 | 33 | -spec profile_many(file:filename(), file:filename()) -> ok. 34 | profile_many(Wildcard, Output) -> 35 | InitialState = exp1_init(Output), 36 | Files = filelib:wildcard(Wildcard), 37 | FinalState = lists:foldl(fun(Input, State0) -> 38 | case lg_file_reader:fold(fun handle_event/2, State0, Input) of 39 | {ok, State} -> 40 | State; 41 | {error, Reason, HumanReadable} -> 42 | io:format("Error ~p while reading ~s:~n~s~n", 43 | [Reason, Input, HumanReadable]), 44 | State0 45 | end 46 | end, InitialState, Files), 47 | flush(FinalState). 48 | 49 | flush(#state{output_path=OutputPath}) -> 50 | PidStates = get(), 51 | {ok, FH} = file:open(OutputPath, [write, raw, binary, delayed_write]), 52 | io:format("\n\nWriting to ~s for ~w processes... ", [OutputPath, length(PidStates)]), 53 | _ = [ 54 | [begin 55 | Pid_str0 = lists:flatten(io_lib:format("~w", [Pid])), 56 | Size = length(Pid_str0), 57 | Pid_str = [$(, lists:sublist(Pid_str0, 2, Size-2), $)], 58 | Time_str = integer_to_list(Time), 59 | file:write(FH, [Pid_str, $;, intersperse($;, lists:reverse(Stack)), 32, Time_str, 10]) 60 | end || {Stack, Time} <- Acc] 61 | || {Pid, #state{acc=Acc} = _S} <- PidStates], 62 | _ = file:close(FH), 63 | io:format("finished!\n"), 64 | ok. 65 | 66 | handle_event({Type, Pid, Ts, Arg}, State) -> 67 | exp1({trace_ts, Pid, Type, Arg, Ts}, State); 68 | handle_event({Type, Pid, Ts, Arg, ExtraOrMspec}, State) -> 69 | exp1({trace_ts, Pid, Type, Arg, ExtraOrMspec, Ts}, State); 70 | handle_event({Type, Pid, Ts, Arg, Extra, Mspec}, State) -> 71 | exp1({trace_ts, Pid, Type, Arg, Extra, Mspec, Ts}, State). 72 | 73 | %% Below is Scott L. Fritchie's ISC licensed work with only a handful changes. 74 | 75 | exp1_init(OutputPath) -> 76 | #state{output_path=OutputPath}. 77 | 78 | exp1(T, #state{output_path=OutputPath} = S) -> 79 | trace_ts = element(1, T), 80 | Pid = element(2, T), 81 | PidState = case erlang:get(Pid) of 82 | undefined -> 83 | io:format("~p ", [Pid]), 84 | #state{output_path=OutputPath}; 85 | SomeState -> 86 | SomeState 87 | end, 88 | NewPidState = exp1_inner(T, PidState), 89 | erlang:put(Pid, NewPidState), 90 | S. 91 | 92 | exp1_inner({trace_ts, _Pid, InOut, _MFA, _TS}, #state{last_ts=undefined} = S) 93 | when InOut == in; InOut == out -> 94 | %% in & out, without call context, don't help us 95 | S; 96 | exp1_inner({trace_ts, _Pid, Return, _MFA, _TS}, #state{last_ts=undefined} = S) 97 | when Return == return_from; Return == return_to -> 98 | %% return_from and return_to, without call context, don't help us 99 | S; 100 | exp1_inner({trace_ts, Pid, call, MFA, BIN, TS}, 101 | #state{last_ts=LastTS, acc=Acc, count=Count} = S) -> 102 | try 103 | %% Calculate time elapsed, TS-LastTs. 104 | %% 0. If Acc is empty, then skip step #1. 105 | %% 1. Credit elapsed time to the stack on the top of Acc. 106 | %% 2. Push a 0 usec item with this stack onto Acc. 107 | Stak = lists:filter(fun(<<"unknown function">>) -> false; 108 | (_) -> true 109 | end, stak_binify(BIN)), 110 | Stack0 = stak_trim(Stak), 111 | MFA_bin = mfa_binify(MFA), 112 | Stack1 = [MFA_bin|lists:reverse(Stack0)], 113 | Acc2 = case Acc of 114 | [] -> 115 | [{Stack1, 0}]; 116 | [{LastStack, LastTime}|Tail] -> 117 | USec = TS - LastTS, 118 | % io:format("Stack1: ~p ~p\n", [Stack1, USec]), 119 | [{Stack1, 0}, 120 | {LastStack, LastTime + USec}|Tail] 121 | end, 122 | %% TODO: more state tracking here. 123 | S#state{pid=Pid, last_ts=TS, count=Count+1, acc=Acc2} 124 | catch XX:YY:ZZ -> 125 | io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, XX, YY, ZZ]), 126 | S 127 | end; 128 | exp1_inner({trace_ts, _Pid, return_to, MFA, TS}, #state{last_ts=LastTS, acc=Acc} = S) -> 129 | try 130 | %% Calculate time elapsed, TS-LastTs. 131 | %% 1. Credit elapsed time to the stack on the top of Acc. 132 | %% 2. Push a 0 usec item with the "best" stack onto Acc. 133 | %% "best" = MFA exists in the middle of the stack onto Acc, 134 | %% or else MFA exists at the top of a stack elsewhere in Acc. 135 | [{LastStack, LastTime}|Tail] = Acc, 136 | MFA_bin = mfa_binify(MFA), 137 | BestStack = lists:dropwhile(fun(SomeMFA) when SomeMFA /= MFA_bin -> true; 138 | (_) -> false 139 | end, find_matching_stack(MFA_bin, Acc)), 140 | USec = TS - LastTS, 141 | Acc2 = [{BestStack, 0}, 142 | {LastStack, LastTime + USec}|Tail], 143 | % io:format(user, "return-to: ~p\n", [lists:sublist(Acc2, 4)]), 144 | S#state{last_ts=TS, acc=Acc2} 145 | catch XX:YY:ZZ -> 146 | io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, XX, YY, ZZ]), 147 | S 148 | end; 149 | 150 | exp1_inner({trace_ts, _Pid, gc_start, _Info, TS}, #state{last_ts=LastTS, acc=Acc} = S) -> 151 | try 152 | %% Push a 0 usec item onto Acc. 153 | [{LastStack, LastTime}|Tail] = Acc, 154 | NewStack = [<<"GARBAGE-COLLECTION">>|LastStack], 155 | USec = TS - LastTS, 156 | Acc2 = [{NewStack, 0}, 157 | {LastStack, LastTime + USec}|Tail], 158 | % io:format(user, "GC 1: ~p\n", [lists:sublist(Acc2, 4)]), 159 | S#state{last_ts=TS, acc=Acc2} 160 | catch _XX:_YY:_ZZ -> 161 | %% io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, _XX, _YY, _ZZ]), 162 | S 163 | end; 164 | exp1_inner({trace_ts, _Pid, gc_end, _Info, TS}, #state{last_ts=LastTS, acc=Acc} = S) -> 165 | try 166 | %% Push the GC time onto Acc, then push 0 usec item from last exec 167 | %% stack onto Acc. 168 | [{GCStack, GCTime},{LastExecStack,_}|Tail] = Acc, 169 | USec = TS - LastTS, 170 | Acc2 = [{LastExecStack, 0}, {GCStack, GCTime + USec}|Tail], 171 | % io:format(user, "GC 2: ~p\n", [lists:sublist(Acc2, 4)]), 172 | S#state{last_ts=TS, acc=Acc2} 173 | catch _XX:_YY:_ZZ -> 174 | %% io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, _XX, _YY, _ZZ]), 175 | S 176 | end; 177 | 178 | exp1_inner({trace_ts, _Pid, out, MFA, TS}, #state{last_ts=LastTS, acc=Acc} = S) -> 179 | try 180 | %% Push a 0 usec item onto Acc. 181 | %% The MFA reported here probably doesn't appear in the stacktrace 182 | %% given to us by the last 'call', so add it here. 183 | [{LastStack, LastTime}|Tail] = Acc, 184 | MFA_bin = mfa_binify(MFA), 185 | NewStack = [<<"SLEEP">>,MFA_bin|LastStack], 186 | USec = TS - LastTS, 187 | Acc2 = [{NewStack, 0}, 188 | {LastStack, LastTime + USec}|Tail], 189 | S#state{last_ts=TS, acc=Acc2} 190 | catch XX:YY:ZZ -> 191 | io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, XX, YY, ZZ]), 192 | S 193 | end; 194 | exp1_inner({trace_ts, _Pid, in, MFA, TS}, #state{last_ts=LastTS, acc=Acc} = S) -> 195 | try 196 | %% Push the Sleep time onto Acc, then push 0 usec item from last 197 | %% exec stack onto Acc. 198 | %% The MFA reported here probably doesn't appear in the stacktrace 199 | %% given to us by the last 'call', so add it here. 200 | MFA_bin = mfa_binify(MFA), 201 | [{SleepStack, SleepTime},{LastExecStack,_}|Tail] = Acc, 202 | USec = TS - LastTS, 203 | Acc2 = [{[MFA_bin|LastExecStack], 0}, {SleepStack, SleepTime + USec}|Tail], 204 | S#state{last_ts=TS, acc=Acc2} 205 | catch XX:YY:ZZ -> 206 | io:format(user, "~p: ~p:~p @ ~p\n", [?LINE, XX, YY, ZZ]), 207 | S 208 | end; 209 | 210 | %exp1_inner(end_of_trace = _Else, #state{pid=Pid, output_path=OutputPath, acc=Acc} = S) -> 211 | % {ok, FH} = file:open(OutputPath, [write, raw, binary, delayed_write]), 212 | % io:format("Writing to ~s ... ", [OutputPath]), 213 | % [begin 214 | % Pid_str = io_lib:format("~w", [Pid]), 215 | % Time_str = integer_to_list(Time), 216 | % file:write(FH, [Pid_str, $;, intersperse($;, lists:reverse(Stack)), 32, Time_str, 10]) 217 | % end || {Stack, Time} <- Acc], 218 | % file:close(FH), 219 | % io:format("finished\n"), 220 | % S; 221 | exp1_inner(_Else, S) -> 222 | % io:format("?? ~P\n", [_Else, 10]), 223 | S. 224 | 225 | find_matching_stack(MFA_bin, [{H,_Time}|_] = Acc) -> 226 | case lists:member(MFA_bin, H) of 227 | true -> 228 | H; 229 | false -> 230 | find_matching_stack2(MFA_bin, Acc) 231 | end. 232 | 233 | find_matching_stack2(MFA_bin, [{[MFA_bin|_StackTail]=Stack,_Time}|_]) -> 234 | Stack; 235 | find_matching_stack2(MFA_bin, [_H|T]) -> 236 | find_matching_stack2(MFA_bin, T); 237 | find_matching_stack2(_MFA_bin, []) -> 238 | [<<"FIND-MATCHING-FAILED">>]. 239 | 240 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 241 | 242 | intersperse(_, []) -> []; 243 | intersperse(_, [X]) -> [X]; 244 | intersperse(Sep, [X | Xs]) -> [X, Sep | intersperse(Sep, Xs)]. 245 | 246 | stak_trim([<<"proc_lib:init_p_do_apply/3">>,<<"gen_fsm:decode_msg/9">>,<<"gen_fsm:handle_msg/7">>,<<"gen_fsm:loop/7">>|T]) -> 247 | stak_trim([<<"GEN-FSM">>|T]); 248 | stak_trim([<<"GEN-FSM">>,<<"gen_fsm:decode_msg/9">>,<<"gen_fsm:handle_msg/7">>,<<"gen_fsm:loop/7">>|T]) -> 249 | stak_trim([<<"GEN-FSM">>|T]); 250 | stak_trim(Else) -> 251 | Else. 252 | 253 | stak_binify(Bin) when is_binary(Bin) -> 254 | [list_to_binary(X) || X <- stak(Bin)]; 255 | stak_binify(X) -> 256 | list_to_binary(io_lib:format("~w", [X])). 257 | 258 | mfa_binify({M,F,A}) -> 259 | list_to_binary(io_lib:format("~w:~w/~w", [M, F, A])); 260 | mfa_binify(X) -> 261 | list_to_binary(io_lib:format("~w", [X])). 262 | 263 | %% Borrowed from redbug.erl 264 | 265 | stak(Bin) -> 266 | lists:foldl(fun munge/2,[],string:tokens(binary_to_list(Bin),"\n")). 267 | 268 | munge(I,Out) -> 269 | case I of %% lists:reverse(I) of 270 | "..."++_ -> ["truncated!!!"|Out]; 271 | _ -> 272 | case string:str(I, "Return addr") of 273 | 0 -> 274 | case string:str(I, "cp = ") of 275 | 0 -> Out; 276 | _ -> [mfaf(I)|Out] 277 | end; 278 | _ -> 279 | case string:str(I, "erminate process normal") of 280 | 0 -> [mfaf(I)|Out]; 281 | _ -> Out 282 | end 283 | end 284 | end. 285 | 286 | mfaf(I) -> 287 | [_, C|_] = string:tokens(I,"()+"), 288 | string:strip(C). 289 | -------------------------------------------------------------------------------- /src/lg_messages.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_messages). 16 | 17 | -export([profile/1]). 18 | -export([profile_many/1]). 19 | 20 | -record(state, { 21 | meta = #{} :: map(), 22 | senders = #{} :: #{pid() => pos_integer()}, 23 | receivers = #{} :: #{pid() => pos_integer()}, 24 | pairs = #{} :: #{{pid(), pid()} => pos_integer()}, 25 | non_existing = #{} :: #{pid() => pos_integer()}, 26 | last_msgs = #{} :: #{pid() => atom()} 27 | }). 28 | 29 | -spec profile(file:filename_all()) -> ok. 30 | profile(Input) -> 31 | {ok, FinalState} = lg_file_reader:fold(fun handle_event/2, #state{}, Input), 32 | flush(FinalState). 33 | 34 | -spec profile_many(file:filename()) -> ok. 35 | profile_many(Wildcard) -> 36 | Files = filelib:wildcard(Wildcard), 37 | FinalState = lists:foldl(fun(Input, State0) -> 38 | {ok, State} = lg_file_reader:fold(fun handle_event/2, State0, Input), 39 | State 40 | end, #state{}, Files), 41 | flush(FinalState). 42 | 43 | %% @todo Later we may want to look at the latency of gen_server call/reply. 44 | %% @todo Later we may want to look at particular messages, have some sort of callback. 45 | handle_event({send, From, _, Info, lg}, State=#state{meta=Meta0}) -> 46 | Meta = case Meta0 of 47 | #{From := Info0} -> Meta0#{From => maps:merge(Info0, Info)}; 48 | _ -> Meta0#{From => Info} 49 | end, 50 | State#state{meta=Meta}; 51 | handle_event({send, From, _, Msg, To}, 52 | State=#state{senders=Senders, receivers=Receivers, pairs=Pairs, last_msgs=Msgs}) -> 53 | SendersCount = maps:get(From, Senders, 0), 54 | ReceiversCount = maps:get(To, Receivers, 0), 55 | PairsCount = maps:get({From, To}, Pairs, 0), 56 | State#state{ 57 | senders=Senders#{From => SendersCount + 1}, 58 | receivers=Receivers#{To => ReceiversCount + 1}, 59 | pairs=Pairs#{{From, To} => PairsCount + 1}, 60 | last_msgs=Msgs#{From => Msg}}; 61 | handle_event({send_to_non_existing_process, From, _, Msg, _}, 62 | State=#state{non_existing=Map, last_msgs=Msgs}) -> 63 | Count = maps:get(From, Map, 0), 64 | State#state{ 65 | non_existing=Map#{From => Count + 1}, 66 | last_msgs=Msgs#{From => Msg}}; 67 | %% Ignore all other events. We only care about messages. 68 | handle_event(_, State) -> 69 | State. 70 | 71 | %% Output of the profiling. 72 | 73 | flush(State) -> 74 | flush_most_active_senders(State), 75 | flush_most_active_receivers(State), 76 | flush_most_non_existing(State), 77 | flush_most_active_pair_unidirectional(State), 78 | flush_most_active_pair_bidirectional(State), 79 | io:format("~n"), 80 | flush_digraph(State), 81 | ok. 82 | 83 | flush_most_active_senders(State=#state{senders=Procs}) -> 84 | List = lists:sublist( 85 | lists:reverse(lists:keysort(2, maps:to_list(Procs))), 86 | 1, 100), 87 | format_by_count("They sent the most messages", List, State). 88 | 89 | flush_most_active_receivers(State=#state{receivers=Procs}) -> 90 | List = lists:sublist( 91 | lists:reverse(lists:keysort(2, maps:to_list(Procs))), 92 | 1, 100), 93 | format_by_count("They received the most messages", List, State). 94 | 95 | flush_most_non_existing(State=#state{non_existing=Procs}) -> 96 | List = lists:sublist( 97 | lists:reverse(lists:keysort(2, maps:to_list(Procs))), 98 | 1, 100), 99 | format_by_count("They sent the most messages to dead processes", List, State). 100 | 101 | format_by_count(Title, List, State) -> 102 | MsgCols = case io:columns() of 103 | {ok, Cols} -> Cols; 104 | _ -> 80 105 | end, 106 | io:format( 107 | "~n~s~n~s~n~n" 108 | "Process ID Count (Label) OR Message sent~n" 109 | "---------- ----- -----------------------~n", 110 | [Title, lists:duplicate(length(Title), $=)]), 111 | _ = [begin 112 | {Prefix, Label, Suffix} = label_or_msg(P, State), 113 | io:format("~-15w ~-10b ~s~" ++ integer_to_list(MsgCols) ++ "P~s~n", 114 | [P, C, Prefix, Label, 5, Suffix]) 115 | end || {P, C} <- List], 116 | ok. 117 | 118 | label_or_msg(P, #state{meta=Meta, last_msgs=Msgs}) -> 119 | case maps:get(P, Meta, #{}) of 120 | #{process_type := PT} -> {"(", PT, ")"}; 121 | _ -> {"", maps:get(P, Msgs, ''), ""} 122 | end. 123 | 124 | flush_most_active_pair_unidirectional(State=#state{pairs=Procs}) -> 125 | List = lists:sublist( 126 | lists:reverse(lists:keysort(2, maps:to_list(Procs))), 127 | 1, 100), 128 | Title = "They sent the most messages to one other process", 129 | MsgCols = case io:columns() of 130 | {ok, Cols} -> Cols; 131 | _ -> 80 132 | end, 133 | io:format( 134 | "~n~s~n~s~n~n" 135 | "From pid To pid Count (Label) OR Message sent~n" 136 | "-------- ------ ----- -----------------------~n", 137 | [Title, lists:duplicate(length(Title), $=)]), 138 | _ = [begin 139 | {Prefix, Label, Suffix} = label_or_msg(F, State), 140 | io:format("~-15w ~-15w ~-10b ~s~" ++ integer_to_list(MsgCols) ++ "P~s~n", 141 | [F, T, C, Prefix, Label, 5, Suffix]) 142 | end || {{F, T}, C} <- List], 143 | ok. 144 | 145 | flush_most_active_pair_bidirectional(State=#state{pairs=Procs0}) -> 146 | Procs = maps:fold(fun merge_pairs/3, #{}, Procs0), 147 | List = lists:sublist( 148 | lists:reverse(lists:keysort(2, maps:to_list(Procs))), 149 | 1, 100), 150 | Title = "They sent the most messages to each other", 151 | MsgCols = case io:columns() of 152 | {ok, Cols} -> Cols; 153 | _ -> 80 154 | end, 155 | io:format( 156 | "~n~s~n~s~n~n" 157 | "Count Pid 1 (Label) OR Message sent~n" 158 | " Pid 2 by the corresponding process~n" 159 | "----- ----- ----------------------------~n", 160 | [Title, lists:duplicate(length(Title), $=)]), 161 | _ = [begin 162 | {FPrefix, FLabel, FSuffix} = label_or_msg(F, State), 163 | {TPrefix, TLabel, TSuffix} = label_or_msg(T, State), 164 | io:format( 165 | "~-10b ~-15w ~s~" ++ integer_to_list(MsgCols) ++ "P~s~n" 166 | " ~-15w ~s~" ++ integer_to_list(MsgCols) ++ "P~s~n", 167 | [C, F, FPrefix, FLabel, 5, FSuffix, 168 | T, TPrefix, TLabel, 5, TSuffix]) 169 | end || {{F, T}, C} <- List], 170 | ok. 171 | 172 | flush_digraph(State=#state{pairs=Procs0}) -> 173 | Procs = maps:fold(fun group_pairs/3, #{}, Procs0), 174 | List = maps:to_list(Procs), 175 | ok = file:write_file("digraph.gv", [ 176 | "digraph {\n" 177 | " concentrate=true;\n" 178 | " splines=ortho;\n" 179 | " edge [arrowhead=none, labelfontsize=12.0, minlen=3];\n" 180 | "\n", 181 | [io_lib:format(" \"~w~s\" -> \"~w~s\" [taillabel=~b, headlabel=~b];~n", 182 | [F, label(F, State), T, label(T, State), FC, TC]) || {{F, T}, {FC, TC}} <- List], 183 | "}\n" 184 | ]), 185 | io:format( 186 | "The file digraph.gv was created. Use GraphViz to make a PNG.~n" 187 | "$ dot -Tpng -O digraph.gv~n" 188 | "~n" 189 | "You can also edit the file to remove uninteresting processes.~n" 190 | "One line in the file is equal to a connection between two processes.~n"), 191 | ok. 192 | 193 | label(P, #state{meta=Meta}) -> 194 | case maps:get(P, Meta, #{}) of 195 | #{process_type := PT} -> io_lib:format(" (~w)", [PT]); 196 | _ -> "" 197 | end. 198 | 199 | merge_pairs({From, To}, Count, Acc) -> 200 | Key = if 201 | From < To -> {From, To}; 202 | true -> {To, From} 203 | end, 204 | AccCount = maps:get(Key, Acc, 0), 205 | Acc#{Key => AccCount + Count}. 206 | 207 | group_pairs({From, To}, Count, Acc) when From < To -> 208 | Key = {From, To}, 209 | {_, AccCount} = maps:get(Key, Acc, {0, 0}), 210 | Acc#{Key => {Count, AccCount}}; 211 | group_pairs({From, To}, Count, Acc) -> 212 | Key = {To, From}, 213 | {AccCount, _} = maps:get(Key, Acc, {0, 0}), 214 | Acc#{Key => {AccCount, Count}}. 215 | -------------------------------------------------------------------------------- /src/lg_messages_seqdiag.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_messages_seqdiag). 16 | 17 | -export([profile/2]). 18 | -export([profile_many/2]). 19 | 20 | -record(state, { 21 | meta = #{} :: map(), 22 | events = [], 23 | pids 24 | }). 25 | 26 | -spec profile(file:filename_all(), list()) -> ok. 27 | profile(Input, Pids) -> 28 | {ok, FinalState} = lg_file_reader:fold(fun handle_event/2, 29 | #state{pids=prepare_pids(Pids)}, Input), 30 | flush(FinalState). 31 | 32 | -spec profile_many(file:filename(), list()) -> ok. 33 | profile_many(Wildcard, Pids) -> 34 | Files = filelib:wildcard(Wildcard), 35 | FinalState = lists:foldl(fun(Input, State0) -> 36 | {ok, State} = lg_file_reader:fold(fun handle_event/2, State0, Input), 37 | State 38 | end, #state{pids=prepare_pids(Pids)}, Files), 39 | flush(FinalState). 40 | 41 | handle_event({send, From, _, Info, lg}, State=#state{meta=Meta0}) -> 42 | Meta = case Meta0 of 43 | #{From := Info0} -> Meta0#{From => maps:merge(Info0, Info)}; 44 | _ -> Meta0#{From => Info} 45 | end, 46 | State#state{meta=Meta}; 47 | handle_event(Event = {Type, From, _, _, To}, State) 48 | when Type =:= send; Type =:= send_to_non_existing_process -> 49 | maybe_keep_event(Event, From, To, State); 50 | handle_event(Event = {spawn, From, _, To, _}, State) -> 51 | maybe_keep_event(Event, From, To, State); 52 | handle_event(Event = {exit, Pid0, _, _}, State=#state{events=Events, pids=Pids}) -> 53 | Pid = hide_pid_node(Pid0), 54 | case lists:member(Pid, Pids) of 55 | true -> State#state{events=[Event|Events]}; 56 | _ -> State 57 | end; 58 | %% Ignore all other events. We only care about messages and spawns/exits. 59 | handle_event(_, State) -> 60 | State. 61 | 62 | maybe_keep_event(Event, From0, To0, State=#state{events=Events, pids=Pids}) -> 63 | From = hide_pid_node(From0), 64 | To = hide_pid_node(To0), 65 | case {lists:member(From, Pids), lists:member(To, Pids)} of 66 | {true, true} -> State#state{events=[Event|Events]}; 67 | _ -> State 68 | end. 69 | 70 | prepare_pids(Pids) -> 71 | [hide_pid_node(Pid) || Pid <- Pids]. 72 | 73 | hide_pid_node(Pid) when is_pid(Pid) -> hide_pid_node(pid_to_list(Pid)); 74 | hide_pid_node([$<, _, $.|Tail]) -> "<***." ++ Tail; 75 | hide_pid_node([$<, _, _, $.|Tail]) -> "<***." ++ Tail; 76 | hide_pid_node([$<, _, _, _, $.|Tail]) -> "<***." ++ Tail; 77 | hide_pid_node([$<, _, _, _, _, $.|Tail]) -> "<***." ++ Tail; 78 | hide_pid_node([$<, _, _, _, _, _, $.|Tail]) -> "<***." ++ Tail; 79 | hide_pid_node(Name) -> Name. 80 | 81 | flush(State=#state{events=Events0}) -> 82 | %% Sort by timestamp from oldest to newest. 83 | Events = lists:keysort(3, Events0), 84 | %% Initialize the formatting state. 85 | put(num_calls, 0), 86 | %% Output everything. 87 | ok = file:write_file("seq.diag", [ 88 | "seqdiag {\n" 89 | " edge_length = 300;\n" 90 | " activation = none;\n" 91 | "\n", 92 | [format_event(Event, State) || Event <- Events], 93 | "}\n" 94 | ]), 95 | io:format( 96 | "The file seq.diag was created. Use seqdiag to make a PNG.~n" 97 | "$ seqdiag -Tpng --no-transparency seq.diag~n" 98 | "~n" 99 | "To use a custom font, use the -f modifier:~n" 100 | "$ seqdiag -Tpng --no-transparency -f /usr/share/fonts/TTF/verdana.ttf seq.diag~n" 101 | "~n" 102 | "You can also edit the file to remove uninteresting messages.~n" 103 | "One line in the file is equal to a message sent by a process to another.~n"), 104 | ok. 105 | 106 | format_event({spawn, From, _, To, MFA}, State) -> 107 | io_lib:format(" \"~w~s\" ->> \"~w~s\" [label=\"spawn ~9999P\"];~n", [ 108 | From, label(From, State), To, label(To, State), MFA, 8]); 109 | format_event({exit, Pid, _, Reason}, State) -> 110 | PidLabel = label(Pid, State), 111 | io_lib:format(" \"~w~s\" ->> \"~w~s\" [label=\"exit ~9999P\"];~n", [ 112 | Pid, PidLabel, Pid, PidLabel, Reason, 8]); 113 | format_event({Type, From, _, {'$gen_call', {From, Ref}, Msg}, To}, State) -> 114 | NumCalls = get(num_calls) + 1, 115 | put(num_calls, NumCalls), 116 | put(Ref, NumCalls), 117 | io_lib:format(" \"~w~s\" ~s \"~w~s\" [label=\"gen:call #~w ~9999P\"];~n", [ 118 | From, label(From, State), 119 | case Type of send -> "->"; _ -> "-->" end, 120 | To, label(To, State), NumCalls, Msg, 8]); 121 | format_event(Event={Type, From, _, {Ref, Msg}, To}, State) -> 122 | case get(Ref) of 123 | undefined -> 124 | default_format_event(Event, State); 125 | NumCall -> 126 | io_lib:format(" \"~w~s\" ~s \"~w~s\" [label=\"#~w ~9999P\"];~n", [ 127 | From, label(From, State), 128 | case Type of send -> "->"; _ -> "-->" end, 129 | To, label(To, State), NumCall, Msg, 8]) 130 | end; 131 | format_event(Event, State) -> 132 | default_format_event(Event, State). 133 | 134 | default_format_event({Type, From, _, Msg, To}, State) -> 135 | io_lib:format(" \"~w~s\" ~s \"~w~s\" [label=\"~9999P\"];~n", [ 136 | From, label(From, State), 137 | case Type of send -> "->"; _ -> "-->" end, 138 | To, label(To, State), Msg, 8]). 139 | 140 | label(P, #state{meta=Meta}) -> 141 | case maps:get(P, Meta, #{}) of 142 | #{process_type := PT} -> io_lib:format(" (~w)", [PT]); 143 | _ -> "" 144 | end. 145 | -------------------------------------------------------------------------------- /src/lg_rabbit_hole.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | %% The purpose of this process is to be the target of messages 16 | %% sent by traced processes. The messages contain metadata that 17 | %% we want to log when we are tracing and later use when profiling 18 | %% the sending of messages. This process does not need them, it 19 | %% just needs to exist, and therefore it discards everything. 20 | 21 | -module(lg_rabbit_hole). 22 | -behaviour(gen_server). 23 | 24 | %% API. 25 | -export([start_link/0]). 26 | 27 | %% gen_server. 28 | -export([init/1]). 29 | -export([handle_call/3]). 30 | -export([handle_cast/2]). 31 | -export([handle_info/2]). 32 | -export([terminate/2]). 33 | -export([code_change/3]). 34 | 35 | %% API. 36 | 37 | -spec start_link() -> {ok, pid()}. 38 | start_link() -> 39 | gen_server:start_link({local, lg}, ?MODULE, [], []). 40 | 41 | %% gen_server. 42 | 43 | init([]) -> 44 | {ok, undefined}. 45 | 46 | handle_call(_Request, _From, State) -> 47 | {reply, ignored, State}. 48 | 49 | handle_cast(_Msg, State) -> 50 | {noreply, State}. 51 | 52 | handle_info(_Info, State) -> 53 | {noreply, State}. 54 | 55 | terminate(_Reason, _State) -> 56 | ok. 57 | 58 | code_change(_OldVsn, State, _Extra) -> 59 | {ok, State}. 60 | -------------------------------------------------------------------------------- /src/lg_raw_console_tracer.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_raw_console_tracer). 16 | 17 | -export([start_link/2]). 18 | -export([init/1]). 19 | -export([loop/1]). 20 | 21 | -export([system_continue/3]). 22 | -export([system_terminate/4]). 23 | -export([system_code_change/4]). 24 | 25 | start_link(_Nth, _Opts) -> 26 | Pid = proc_lib:spawn_link(?MODULE, init, [self()]), 27 | {ok, Pid}. 28 | 29 | init(Parent) -> 30 | %% Store all messages off the heap to avoid unnecessary GC. 31 | process_flag(message_queue_data, off_heap), 32 | loop(Parent). 33 | 34 | loop(Parent) -> 35 | receive 36 | {system, From, Request} -> 37 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], Parent); 38 | Msg0 -> 39 | %% Convert the event's monotonic time to its system time. 40 | Msg = setelement(3, Msg0, erlang:time_offset(microsecond) + element(3, Msg0)), 41 | erlang:display(Msg), 42 | loop(Parent) 43 | end. 44 | 45 | system_continue(_, _, Parent) -> 46 | loop(Parent). 47 | 48 | -spec system_terminate(any(), _, _, _) -> no_return(). 49 | system_terminate(Reason, _, _, _) -> 50 | exit(Reason). 51 | 52 | system_code_change(Misc, _, _, _) -> 53 | {ok, Misc}. 54 | -------------------------------------------------------------------------------- /src/lg_socket_client.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_socket_client). 16 | -behavior(gen_statem). 17 | 18 | -export([start_link/2]). 19 | -export([stop/1]). 20 | 21 | %% gen_statem. 22 | -export([callback_mode/0]). 23 | -export([init/1]). 24 | -export([connect/3]). 25 | -export([open_file/3]). 26 | -export([process_events/3]). 27 | -export([close_file/3]). 28 | -export([code_change/4]). 29 | -export([terminate/3]). 30 | 31 | -record(state, { 32 | port :: inet:port_number(), 33 | base_filename :: file:filename_all(), 34 | nth = 0 :: non_neg_integer(), 35 | socket :: inet:socket() | undefined, 36 | io_device :: file:io_device() | undefined, 37 | events_per_frame = 100000 :: pos_integer(), 38 | events_this_frame = 0 :: non_neg_integer(), 39 | buffer = <<>> :: binary() 40 | }). 41 | 42 | start_link(Port, BaseFilename) -> 43 | gen_statem:start_link(?MODULE, [Port, BaseFilename], []). 44 | 45 | stop(Pid) -> 46 | gen_statem:stop(Pid). 47 | 48 | callback_mode() -> 49 | state_functions. 50 | 51 | init([Port, BaseFilename]) -> 52 | %% Store all messages off the heap to avoid unnecessary GC. 53 | process_flag(message_queue_data, off_heap), 54 | %% We need to trap exit signals in order to shutdown properly. 55 | process_flag(trap_exit, true), 56 | {ok, connect, #state{port=Port, base_filename=BaseFilename}, 57 | {next_event, internal, run}}. 58 | 59 | connect(internal, _, State) -> 60 | do_connect(State); 61 | connect({timeout, retry}, retry, State) -> 62 | do_connect(State); 63 | connect(_, _, State) -> 64 | {keep_state, State}. 65 | 66 | do_connect(State=#state{port=Port}) -> 67 | case gen_tcp:connect("localhost", Port, [binary, {packet, 2}, {active, true}]) of 68 | {ok, Socket} -> 69 | {next_state, open_file, State#state{socket=Socket}, 70 | {next_event, internal, run}}; 71 | {error, _} -> 72 | {keep_state, State, [{{timeout, retry}, 1000, retry}]} 73 | end. 74 | 75 | open_file(internal, _, State=#state{base_filename=Filename0, nth=Nth}) -> 76 | Filename = filename:flatten([Filename0, ".", integer_to_list(Nth)]), 77 | {ok, IoDevice} = file:open(Filename, [write, raw]), 78 | {next_state, process_events, State#state{nth=Nth + 1, io_device=IoDevice}}. 79 | 80 | process_events(info, {tcp, Socket, Bin}, State=#state{socket=Socket, io_device=IoDevice, 81 | events_per_frame=MaxEvents, events_this_frame=NumEvents0, buffer=Buffer0}) -> 82 | BinSize = byte_size(Bin), 83 | Buffer = <>, 84 | NumEvents = NumEvents0 + 1, 85 | if 86 | MaxEvents =:= NumEvents -> 87 | ok = file:write(IoDevice, lz4f:compress_frame(Buffer)), 88 | {keep_state, State#state{events_this_frame=0, buffer= <<>>}}; 89 | true -> 90 | {keep_state, State#state{events_this_frame=NumEvents, buffer=Buffer}} 91 | end; 92 | process_events(info, {tcp_closed, Socket}, State=#state{socket=Socket}) -> 93 | {next_state, close_file, State#state{socket=undefined}, 94 | {next_event, internal, run}}; 95 | process_events(info, {tcp_error, Socket, _}, State=#state{socket=Socket}) -> 96 | _ = gen_tcp:close(Socket), 97 | {next_state, close_file, State#state{socket=undefined}, 98 | {next_event, internal, run}}. 99 | 100 | close_file(internal, _, State) -> 101 | do_close_file(State), 102 | {next_state, connect, State#state{io_device=undefined}, 103 | {next_event, internal, run}}. 104 | 105 | do_close_file(#state{io_device=IoDevice, buffer=Buffer}) -> 106 | _ = file:write(IoDevice, lz4f:compress_frame(Buffer)), 107 | _ = file:close(IoDevice), 108 | ok. 109 | 110 | code_change(_OldVsn, OldState, OldData, _Extra) -> 111 | {callback_mode(), OldState, OldData}. 112 | 113 | terminate(_, _, #state{io_device=undefined}) -> 114 | ok; 115 | terminate(_, _, State) -> 116 | do_close_file(State), 117 | ok. 118 | -------------------------------------------------------------------------------- /src/lg_socket_tracer.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_socket_tracer). 16 | 17 | -export([start_link/2]). 18 | -export([init/2]). 19 | 20 | -export([system_continue/3]). 21 | -export([system_terminate/4]). 22 | -export([system_code_change/4]). 23 | 24 | -record(state, { 25 | parent :: pid(), 26 | lsocket :: inet:socket(), 27 | timeout_ref :: reference() | undefined 28 | }). 29 | 30 | start_link(Nth, BasePort) -> 31 | Pid = proc_lib:spawn_link(?MODULE, init, [self(), BasePort + Nth - 1]), 32 | {ok, Pid}. 33 | 34 | init(Parent, Port) -> 35 | %% Store all messages off the heap to avoid unnecessary GC. 36 | process_flag(message_queue_data, off_heap), 37 | %% We need to trap exit signals in order to shutdown properly. 38 | process_flag(trap_exit, true), 39 | %% Open the listening socket. 40 | {ok, LSocket} = gen_tcp:listen(Port, [ 41 | binary, {reuseaddr, true}, {nodelay, true}, 42 | %% We encode all events to binary inside a 2-byte length frame. 43 | {packet, 2}, 44 | %% We expect the client to send pings every second or so and 45 | %% nothing else, so using active mode is faster and still safe. 46 | {active, true}, 47 | %% We only expect one connection at a time. We don't need 48 | %% a backlog except for the cases where the connection is 49 | %% lost and will reconnect immediately before we get a 50 | %% chance to accept again. 51 | {backlog, 1} 52 | %% We are using non-blocking TCP send. We therefore do not 53 | %% need to configure send timeout options. 54 | ]), 55 | %% We reject all messages until we get a connection. 56 | accept(#state{parent=Parent, lsocket=LSocket}). 57 | 58 | accept(State=#state{lsocket=LSocket}) -> 59 | {ok, AcceptRef} = prim_inet:async_accept(LSocket, -1), 60 | accept_loop(State, AcceptRef). 61 | 62 | accept_loop(State=#state{parent=Parent, lsocket=LSocket}, AcceptRef) -> 63 | receive 64 | {'EXIT', Parent, Reason} -> 65 | exit(Reason); 66 | {system, From, Request} -> 67 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], 68 | {accept_loop, State, AcceptRef}); 69 | {inet_async, LSocket, AcceptRef, {ok, CSocket}} -> 70 | trace_loop(set_timeout(State), CSocket); 71 | {inet_async, LSocket, AcceptRef, Error} -> 72 | exit({accept_error, Error}); 73 | %% We discard all trace events when no client is connected. 74 | %% We may also end up discarding old timeouts or TCP messages. 75 | _ -> 76 | accept_loop(State, AcceptRef) 77 | end. 78 | 79 | trace_loop(State=#state{parent=Parent, timeout_ref=TRef}, CSocket) -> 80 | receive 81 | {'EXIT', Parent, Reason} -> 82 | exit(Reason); 83 | {system, From, Request} -> 84 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], 85 | {trace_loop, State, CSocket}); 86 | %% Reset the timeout when we receive data. 87 | {tcp, CSocket, _} -> 88 | trace_loop(reset_timeout(State), CSocket); 89 | {tcp_closed, CSocket} -> 90 | close(State, CSocket); 91 | {tcp_error, CSocket, _} -> 92 | close(State, CSocket); 93 | {timeout, TRef, ?MODULE} -> 94 | close(State, CSocket); 95 | %% Discard the non-blocking send reply when successful. 96 | {inet_reply, CSocket, ok} -> 97 | trace_loop(State, CSocket); 98 | %% And close the socket when an error occured. 99 | {inet_reply, CSocket, _} -> 100 | close(State, CSocket); 101 | %% Discard TCP messages from closed sockets. 102 | {tcp, _, _} -> 103 | trace_loop(State, CSocket); 104 | {tcp_closed, _} -> 105 | trace_loop(State, CSocket); 106 | {tcp_error, _, _} -> 107 | trace_loop(State, CSocket); 108 | %% Discard any previous timeout. 109 | {timeout, _, ?MODULE} -> 110 | trace_loop(State, CSocket); 111 | Msg -> 112 | Bin = term_to_binary(Msg), 113 | _ = byte_size(Bin), 114 | case erlang:port_command(CSocket, <>, [nosuspend]) of 115 | true -> 116 | trace_loop(State, CSocket); 117 | %% The send buffer is full. 118 | false -> 119 | close(State, CSocket) 120 | end 121 | end. 122 | 123 | close(State, CSocket) -> 124 | _ = gen_tcp:close(CSocket), 125 | accept(cancel_timeout(State)). 126 | 127 | system_continue(_, _, {accept_loop, State, AcceptRef}) -> 128 | accept_loop(State, AcceptRef); 129 | system_continue(_, _, {trace_loop, State, CSocket}) -> 130 | trace_loop(State, CSocket). 131 | 132 | -spec system_terminate(any(), _, _, _) -> no_return(). 133 | system_terminate(Reason, _, _, _) -> 134 | exit(Reason). 135 | 136 | system_code_change(Misc, _, _, _) -> 137 | {ok, Misc}. 138 | 139 | reset_timeout(State) -> 140 | set_timeout(cancel_timeout(State)). 141 | 142 | set_timeout(State) -> 143 | TRef = erlang:start_timer(5000, self(), ?MODULE), 144 | State#state{timeout_ref=TRef}. 145 | 146 | cancel_timeout(State=#state{timeout_ref=TRef}) -> 147 | _ = erlang:cancel_timer(TRef, [{async, true}, {info, false}]), 148 | State#state{timeout_ref=undefined}. 149 | -------------------------------------------------------------------------------- /src/lg_term.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | %% Going for hardcoded values for now. We can't spend time 16 | %% looking up inside a record or map for this. 17 | 18 | -module(lg_term). 19 | 20 | -export([truncate/1]). 21 | -export([truncate/2]). 22 | 23 | -define(MAX_DEPTH, 5). 24 | -define(MAX_BINARY_SIZE, 128). 25 | -define(MAX_BITSTRING_SIZE, ?MAX_BINARY_SIZE * 8). 26 | -define(MAX_DATA_STRUCTURES, 5). 27 | -define(MAX_LIST_LENGTH, 32). 28 | -define(MAX_MAP_SIZE, 32). 29 | -define(MAX_TUPLE_SIZE, 32). 30 | 31 | truncate(Term) -> 32 | truncate(Term, 1). 33 | 34 | truncate(_, Depth) when Depth > ?MAX_DEPTH -> 35 | '$truncated'; 36 | truncate(Term, _) when bit_size(Term) > ?MAX_BITSTRING_SIZE -> 37 | <> = Term, 38 | <>; 39 | truncate(Term, Depth) when is_list(Term), Depth =:= ?MAX_DEPTH -> 40 | ['$truncated']; 41 | truncate(Term, Depth) when is_list(Term) -> 42 | truncate_list(Term, Depth, 0, ?MAX_LIST_LENGTH, 0); 43 | truncate(Term, Depth) when is_map(Term), Depth =:= ?MAX_DEPTH -> 44 | #{'$truncated' => '$truncated'}; 45 | truncate(Term, Depth) when is_map(Term) -> 46 | maps:from_list(truncate_map(maps_to_list(Term, ?MAX_MAP_SIZE), Depth, 0)); 47 | truncate(Term, Depth) when is_tuple(Term), Depth =:= ?MAX_DEPTH -> 48 | {'$truncated'}; 49 | truncate(Term, Depth) when is_tuple(Term) -> 50 | list_to_tuple(truncate_list(tuple_to_list(Term), Depth, 0, ?MAX_TUPLE_SIZE, 0)); 51 | truncate(Term, _) -> 52 | Term. 53 | 54 | truncate_list([], _, _, _, _) -> 55 | []; 56 | truncate_list(_, _, Len, MaxLen, _) when Len > MaxLen -> 57 | ['$truncated']; 58 | truncate_list(_, _, _, _, NumStructs) when NumStructs > ?MAX_DATA_STRUCTURES -> 59 | ['$truncated']; 60 | truncate_list([Term|Tail], Depth, Len, MaxLen, NumStructs) -> 61 | [truncate(Term, Depth + 1) 62 | %% if List was a cons, Tail can be anything 63 | |truncate_list(Tail, Depth, Len + 1, MaxLen, NumStructs + is_struct(Term))]; 64 | truncate_list(Term, Depth, _, _, _) -> %% if List was a cons 65 | truncate(Term, Depth + 1). 66 | 67 | truncate_map([], _, _) -> 68 | []; 69 | truncate_map(_, _, NumStructs) when NumStructs > ?MAX_DATA_STRUCTURES -> 70 | [{'$truncated', '$truncated'}]; 71 | truncate_map([{Key, Value}|Tail], Depth, NumStructs) -> 72 | AddStruct = is_struct(Key) + is_struct(Value), 73 | [{truncate(Key, Depth + 1), truncate(Value, Depth + 1)} 74 | |truncate_map(Tail, Depth, NumStructs + AddStruct)]. 75 | 76 | is_struct(Term) -> 77 | if 78 | is_list(Term) -> 1; 79 | is_map(Term) -> 1; 80 | is_tuple(Term) -> 1; 81 | true -> 0 82 | end. 83 | 84 | %% Map iterators were introduced in Erlang/OTP 21. They replace 85 | %% the undocumented function erts_internal:maps_to_list/2. 86 | -ifdef(OTP_RELEASE). 87 | 88 | maps_to_list(Map, MaxSize) -> 89 | I = maps:iterator(Map), 90 | maps_to_list(maps:next(I), MaxSize, []). 91 | 92 | %% Returns elements in arbitrary order. We reverse when we truncate 93 | %% so that the truncated elements come at the end to avoid having 94 | %% two truncated elements in the final output. 95 | maps_to_list(none, _, Acc) -> 96 | Acc; 97 | maps_to_list(_, 0, Acc) -> 98 | lists:reverse([{'$truncated', '$truncated'}|Acc]); 99 | maps_to_list({K, V, I}, N, Acc) -> 100 | maps_to_list(maps:next(I), N - 1, [{K, V}|Acc]). 101 | 102 | -else. 103 | 104 | maps_to_list(Map, MaxSize) -> 105 | erts_internal:maps_to_list(Map, MaxSize). 106 | 107 | -endif. 108 | 109 | -ifdef(TEST). 110 | 111 | maps_to_list_test() -> 112 | [] = maps_to_list(#{}, 10), 113 | [{'$truncated', '$truncated'}] = maps_to_list(#{a => b}, 0), 114 | [{a, b}] = maps_to_list(#{a => b}, 10), 115 | [{a, b}, {c, d}, {e, f}] = lists:sort(maps_to_list( 116 | #{a => b, c => d, e => f}, 3)), 117 | [{'$truncated', '$truncated'}, {a, b}, {c, d}, {e, f}] = lists:sort(maps_to_list( 118 | #{a => b, c => d, e => f, g => h}, 3)), 119 | [{'$truncated', '$truncated'}, {a, b}, {c, d}, {_, _}] = lists:sort(maps_to_list( 120 | #{a => b, c => d, e => f, g => h, i => j}, 3)), 121 | %% Confirm that truncated values are at the end. 122 | [_, _, _, {'$truncated', '$truncated'}] = maps_to_list( 123 | #{a => b, c => d, e => f, g => h, i => j}, 3), 124 | ok. 125 | 126 | -endif. 127 | -------------------------------------------------------------------------------- /src/lg_tracer.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_tracer). 16 | %-behavior(erl_tracer). 17 | 18 | -export([enabled/3]). 19 | -export([enabled_call/3]). 20 | -export([enabled_procs/3]). 21 | -export([enabled_running_procs/3]). 22 | -export([enabled_send/3]). 23 | -export([trace/5]). 24 | 25 | -on_load(on_load/0). 26 | on_load() -> 27 | case code:priv_dir(looking_glass) of 28 | {error, _} -> 29 | {error, {load_failed, "Could not determine the looking_glass priv/ directory."}}; 30 | Path -> 31 | erlang:load_nif(filename:join(Path, atom_to_list(?MODULE)), 0) 32 | end. 33 | 34 | enabled(_, _, _) -> 35 | erlang:nif_error({not_loaded, ?MODULE}). 36 | 37 | enabled_call(_, _, _) -> 38 | erlang:nif_error({not_loaded, ?MODULE}). 39 | 40 | enabled_procs(_, _, _) -> 41 | erlang:nif_error({not_loaded, ?MODULE}). 42 | 43 | enabled_running_procs(_, _, _) -> 44 | erlang:nif_error({not_loaded, ?MODULE}). 45 | 46 | enabled_send(_, _, _) -> 47 | erlang:nif_error({not_loaded, ?MODULE}). 48 | 49 | trace(_, _, _, _, _) -> 50 | erlang:nif_error({not_loaded, ?MODULE}). 51 | -------------------------------------------------------------------------------- /src/lg_tracer_pool.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(lg_tracer_pool). 16 | -behaviour(supervisor). 17 | 18 | -export([start_link/3]). 19 | -export([init/1]). 20 | -export([tracers/1]). 21 | 22 | start_link(NumTracers, TracerMod, Opts) -> 23 | supervisor:start_link(?MODULE, [NumTracers, TracerMod, Opts]). 24 | 25 | init([NumTracers, TracerMod, Opts]) -> 26 | Procs = [#{ 27 | id => {tracer, N}, 28 | start => {TracerMod, start_link, [N, Opts]}, 29 | restart => temporary 30 | } || N <- lists:seq(1, NumTracers)], 31 | {ok, {#{strategy => one_for_all}, Procs}}. 32 | 33 | tracers(PoolPid) -> 34 | [Pid || {_, Pid, _, _} <- supervisor:which_children(PoolPid)]. 35 | -------------------------------------------------------------------------------- /src/looking_glass_app.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(looking_glass_app). 16 | -behaviour(application). 17 | 18 | -export([start/2]). 19 | -export([stop/1]). 20 | 21 | start(_Type, _Args) -> 22 | looking_glass_sup:start_link(). 23 | 24 | stop(_State) -> 25 | ok. 26 | -------------------------------------------------------------------------------- /src/looking_glass_sup.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2017-Present Pivotal Software, Inc. All rights reserved. 2 | %% 3 | %% This package, Looking Glass, is double-licensed under the Mozilla 4 | %% Public License 1.1 ("MPL") and the Apache License version 2 5 | %% ("ASL"). For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL, 6 | %% please see LICENSE-APACHE2. 7 | %% 8 | %% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 9 | %% either express or implied. See the LICENSE file for specific language governing 10 | %% rights and limitations of this software. 11 | %% 12 | %% If you have any questions regarding licensing, please contact us at 13 | %% info@rabbitmq.com. 14 | 15 | -module(looking_glass_sup). 16 | -behaviour(supervisor). 17 | 18 | -export([start_link/0]). 19 | -export([init/1]). 20 | 21 | start_link() -> 22 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 23 | 24 | init([]) -> 25 | Procs = [ 26 | {lg_rabbit_hole, {lg_rabbit_hole, start_link, []}, 27 | permanent, 5000, worker, [lg_rabbit_hole]} 28 | ], 29 | {ok, {{one_for_one, 1, 5}, Procs}}. 30 | -------------------------------------------------------------------------------- /test/lg_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lg_SUITE). 2 | -compile(export_all). 3 | -compile(nowarn_export_all). 4 | 5 | -import(ct_helper, [config/2]). 6 | -import(ct_helper, [doc/1]). 7 | 8 | %% ct. 9 | 10 | all() -> 11 | [{group, all}]. 12 | 13 | %% We cannot run the tests in parallel or they would 14 | %% interfere with each other. 15 | groups() -> 16 | [{all, [], ct_helper:all(?MODULE)}]. 17 | 18 | %% Tests. 19 | 20 | app(Config) -> 21 | doc("Trace a specific application."), 22 | lg:trace({app, stdlib}, lg_file_tracer, config(priv_dir, Config) ++ "/app.lz4"), 23 | lists:seq(1,10), 24 | lg:stop(), 25 | do_ensure_decompress(config(priv_dir, Config) ++ "/app.lz4"). 26 | 27 | callback(Config) -> 28 | doc("Trace using patterns from a callback function."), 29 | lg:trace({callback, ?MODULE, do_callback}, lg_file_tracer, 30 | config(priv_dir, Config) ++ "/callback.lz4"), 31 | lists:seq(1,10), 32 | lg:stop(), 33 | do_ensure_decompress(config(priv_dir, Config) ++ "/callback.lz4"). 34 | 35 | do_callback() -> 36 | [{scope, [self()]}, lists]. 37 | 38 | callgrind_running(Config) -> 39 | doc("Save events to files on disk then build callgrind files."), 40 | PrivDir = config(priv_dir, Config), 41 | lg:trace([{scope, [self()]}, ?MODULE, {app, stdlib}], lg_file_tracer, 42 | PrivDir ++ "/callgrind_running.lz4", 43 | #{mode => profile, running => true}), 44 | do_callgrind_running(), 45 | lg:stop(), 46 | lg_callgrind:profile_many( 47 | PrivDir ++ "/callgrind_running.lz4.*", 48 | PrivDir ++ "/callgrind_running.out", 49 | #{running => true}), 50 | %% For debugging purposes, print the contents of the callgrind.out files. 51 | %% Uncomment for easier debugging, otherwise look into the files directly. 52 | % _ = [begin 53 | % {ok, File} = file:read_file(PrivDir ++ "/callgrind_running.out." ++ integer_to_list(N)), 54 | % io:format(user, "# callgrind_running.out.~p~n~s", [N, File]), 55 | % lg_file_reader:foreach(fun(E) -> io:format(user, "~p~n", [E]) end, 56 | % PrivDir ++ "/callgrind_running.lz4." ++ integer_to_list(N)) 57 | % end || N <- lists:seq(1, erlang:system_info(schedulers))], 58 | ok. 59 | 60 | do_callgrind_running() -> 61 | timer:sleep(1000), 62 | Ref = make_ref(), 63 | erlang:send_after(1000, self(), {go, Ref}), 64 | lists:seq(1,100), 65 | do_callgrind_running_receive(Ref), 66 | lists:seq(1,100), 67 | ok. 68 | 69 | do_callgrind_running_receive(Ref) -> 70 | receive 71 | {go, Ref} -> 72 | ok 73 | end. 74 | 75 | callgrind_running_cycle(Config) -> 76 | doc("Save events to files on disk then build callgrind files. " 77 | "Create a recursive cycle using two functions calling each other."), 78 | PrivDir = config(priv_dir, Config), 79 | lg:trace([{scope, [self()]}, ?MODULE, {app, stdlib}], lg_file_tracer, 80 | PrivDir ++ "/callgrind_running_cycle.lz4", 81 | #{mode => profile, running => true}), 82 | do_callgrind_running_cycle(), 83 | lg:stop(), 84 | lg_callgrind:profile_many( 85 | PrivDir ++ "/callgrind_running_cycle.lz4.*", 86 | PrivDir ++ "/callgrind_running_cycle.out", 87 | #{running => true}), 88 | %% For debugging purposes, print the contents of the callgrind.out files. 89 | %% Uncomment for easier debugging, otherwise look into the files directly. 90 | % _ = [begin 91 | % {ok, File} = file:read_file(PrivDir ++ "/callgrind_running_cycle.out." ++ integer_to_list(N)), 92 | % io:format(user, "# callgrind_running_cycle.out.~p~n~s", [N, File]), 93 | % lg_file_reader:foreach(fun(E) -> io:format(user, "~p~n", [E]) end, 94 | % PrivDir ++ "/callgrind_running_cycle.lz4." ++ integer_to_list(N)) 95 | % end || N <- lists:seq(1, erlang:system_info(schedulers))], 96 | ok. 97 | 98 | do_callgrind_running_cycle() -> 99 | timer:sleep(1000), 100 | lists:seq(1,100), 101 | do_callgrind_running_cycle1(do_callgrind_running_cycle_timer(20)), 102 | lists:seq(1,100), 103 | ok. 104 | 105 | do_callgrind_running_cycle_timer(N) -> 106 | erlang:start_timer(N * 10, self(), N). 107 | 108 | do_callgrind_running_cycle1(Ref) -> 109 | receive 110 | {timeout, Ref, 0} -> 111 | ok; 112 | {timeout, Ref, N} when N rem 5 =:= 0 -> 113 | do_callgrind_running_cycle2(do_callgrind_running_cycle_timer(N - 1)); 114 | {timeout, Ref, N} -> 115 | do_callgrind_running_cycle1(do_callgrind_running_cycle_timer(N - 1)) 116 | end. 117 | 118 | do_callgrind_running_cycle2(Ref) -> 119 | receive 120 | {timeout, Ref, 0} -> 121 | ok; 122 | {timeout, Ref, N} when N rem 4 =:= 0 -> 123 | do_callgrind_running_cycle1(do_callgrind_running_cycle_timer(N - 1)); 124 | {timeout, Ref, N} -> 125 | do_callgrind_running_cycle2(do_callgrind_running_cycle_timer(N - 1)) 126 | end. 127 | 128 | file_tracer(Config) -> 129 | doc("Save events to files on disk."), 130 | lg:trace(lists, lg_file_tracer, config(priv_dir, Config) ++ "/file_tracer.lz4"), 131 | lists:seq(1,10), 132 | lg:stop(), 133 | do_ensure_decompress(config(priv_dir, Config) ++ "/file_tracer.lz4"). 134 | 135 | file_tracer_rotation(Config) -> 136 | doc("Save events to files on disk; rotate the files if they get too big."), 137 | Prefix = config(priv_dir, Config) ++ "/file_tracer.lz4", 138 | lg:trace(lists, lg_file_tracer, #{ 139 | filename_prefix => Prefix, 140 | max_size => 100, %% Intentionally low. 141 | events_per_frame => 10 %% Needed to trigger the rotation, default is too high. 142 | }), 143 | lists:seq(1,1000), 144 | lg:stop(), 145 | %% We should have one or more rotated files. 146 | Result = [begin 147 | Filename = Prefix ++ "." ++ integer_to_list(N) ++ ".bak", 148 | filelib:is_file(Filename) 149 | end || N <- lists:seq(1, erlang:system_info(schedulers))], 150 | true = lists:member(true, lists:usort(Result)), 151 | ok. 152 | 153 | mod(Config) -> 154 | doc("Trace a specific module."), 155 | lg:trace(lists, lg_file_tracer, config(priv_dir, Config) ++ "/mod.lz4"), 156 | lists:seq(1,10), 157 | lg:stop(), 158 | do_ensure_decompress(config(priv_dir, Config) ++ "/mod.lz4"). 159 | 160 | profile_mode(Config) -> 161 | doc("Trace a specific module in profile mode."), 162 | lg:trace(lists, lg_file_tracer, config(priv_dir, Config) ++ "/profile_mode.lz4", 163 | #{mode => profile}), 164 | lists:seq(1,10), 165 | lg:stop(), 166 | do_ensure_decompress(config(priv_dir, Config) ++ "/profile_mode.lz4"). 167 | 168 | raw_console_tracer(_) -> 169 | doc("Print raw events to the console."), 170 | ct:print("Start tracing to the console."), 171 | %% @todo It seems the order matters when starting. Should it? 172 | lg:trace([{scope, [self()]}, lists]), 173 | lists:seq(1,10), 174 | lg:stop(), 175 | ct:print("Stop tracing to the console."). 176 | 177 | running_true(Config) -> 178 | doc("Trace a specific module with running option enabled."), 179 | lg:trace(lists, lg_file_tracer, config(priv_dir, Config) ++ "/running_true.lz4", 180 | #{running => true}), 181 | lists:seq(1,10), 182 | lg:stop(), 183 | do_ensure_decompress(config(priv_dir, Config) ++ "/running_true.lz4"). 184 | 185 | send_true(Config) -> 186 | doc("Trace a specific module with send option enabled."), 187 | lg:trace(lists, lg_file_tracer, config(priv_dir, Config) ++ "/send_true.lz4", 188 | #{send => true}), 189 | Self = self(), 190 | %% Send a message to and from an existing process. 191 | Pid = spawn(fun() -> 192 | receive {msg_from, Self} -> 193 | Self ! {msg_from, self()} 194 | end 195 | end), 196 | Pid ! {msg_from, Self}, 197 | receive {msg_from, Pid} -> ok end, 198 | %% Also send a message to a non existing process. 199 | DeadPid = spawn(fun() -> ok end), 200 | receive after 100 -> ok end, 201 | DeadPid ! {msg_from, Self}, 202 | lg:stop(), 203 | do_ensure_decompress(config(priv_dir, Config) ++ "/send_true.lz4"). 204 | 205 | socket_tracer(_) -> 206 | doc("Send events to a socket."), 207 | Port = 61234, 208 | lg:trace(lists, lg_socket_tracer, Port, #{pool_size => 1}), 209 | {ok, Socket} = gen_tcp:connect("localhost", Port, 210 | [binary, {packet, 2}, {active, true}]), 211 | lists:seq(1,10), 212 | lg:stop(), 213 | do_socket_tracer_recv(Socket). 214 | 215 | socket_tracer_client(Config) -> 216 | doc("Send events to a socket client."), 217 | Port = 61234, 218 | lg:trace(lists, lg_socket_tracer, Port, #{pool_size => 1}), 219 | BaseFilename = config(priv_dir, Config) ++ "/socket_tracer_client.lz4", 220 | {ok, Pid} = lg_socket_client:start_link(Port, BaseFilename), 221 | timer:sleep(1000), 222 | lists:seq(1,10), 223 | lg:stop(), 224 | lg_socket_client:stop(Pid), 225 | {ok, File} = file:read_file(BaseFilename ++ ".0"), 226 | _ = lz4f:decompress(File), 227 | true = filelib:file_size(BaseFilename ++ ".0") > 0, 228 | ok. 229 | 230 | socket_tracer_many(_) -> 231 | doc("Send events to many sockets."), 232 | Port = 61234, 233 | lg:trace(lists, lg_socket_tracer, Port, #{pool_size => 5}), 234 | {ok, _} = gen_tcp:connect("localhost", Port, []), 235 | {ok, _} = gen_tcp:connect("localhost", Port + 1, []), 236 | {ok, _} = gen_tcp:connect("localhost", Port + 2, []), 237 | {ok, _} = gen_tcp:connect("localhost", Port + 3, []), 238 | {ok, _} = gen_tcp:connect("localhost", Port + 4, []), 239 | {error, _} = gen_tcp:connect("localhost", Port + 5, []), 240 | lg:stop(). 241 | 242 | socket_tracer_reconnect(_) -> 243 | doc("Confirm we can reconnect to the tracer."), 244 | Port = 61234, 245 | lg:trace(lists, lg_socket_tracer, Port, #{pool_size => 1}), 246 | {ok, Socket0} = gen_tcp:connect("localhost", Port, 247 | [binary, {packet, 2}, {active, true}]), 248 | ok = gen_tcp:close(Socket0), 249 | {ok, Socket} = gen_tcp:connect("localhost", Port, 250 | [binary, {packet, 2}, {active, true}]), 251 | lists:seq(1,10), 252 | lg:stop(), 253 | do_socket_tracer_recv(Socket). 254 | 255 | do_socket_tracer_recv(Socket) -> 256 | receive 257 | {tcp, Socket, Data} -> 258 | Term = binary_to_term(Data), 259 | true = is_tuple(Term), 260 | do_socket_tracer_recv(Socket); 261 | {tcp_closed, Socket} -> 262 | ok 263 | after 1000 -> 264 | error(timeout) 265 | end. 266 | 267 | stop_while_trace_is_running(Config) -> 268 | doc("Stop tracing while events are still coming in."), 269 | Self = self(), 270 | Pid = spawn_link(fun() -> Self ! {self(), continue}, lists:seq(1,10000000) end), 271 | lg:trace([{scope, [Pid]}, lists], lg_file_tracer, 272 | config(priv_dir, Config) ++ "/stop_while_trace_is_running.lz4"), 273 | receive {Pid, continue} -> ok after 100 -> error(timeout) end, 274 | lg:stop(), 275 | do_ensure_decompress(config(priv_dir, Config) ++ "/stop_while_trace_is_running.lz4"). 276 | 277 | %% Internal. 278 | 279 | do_ensure_decompress(Prefix) -> 280 | %% Ensure the files can be decompressed. 281 | Sizes = [begin 282 | Filename = Prefix ++ "." ++ integer_to_list(N), 283 | {ok, File} = file:read_file(Filename), 284 | _ = lz4f:decompress(File), 285 | filelib:file_size(Filename) 286 | end || N <- lists:seq(1, erlang:system_info(schedulers))], 287 | %% We also need to make sure there is actual data in the files, 288 | %% as lz4f:decompress will succeed when provided with no data. 289 | true = 0 < lists:sum(Sizes), 290 | ok. 291 | -------------------------------------------------------------------------------- /user-template.bazelrc: -------------------------------------------------------------------------------- 1 | # rabbitmqctl wait shells out to 'ps', which is broken in the bazel macOS 2 | # sandbox (https://github.com/bazelbuild/bazel/issues/7448) 3 | # adding "--spawn_strategy=local" to the invocation is a workaround 4 | build --spawn_strategy=local 5 | 6 | # don't re-run flakes automatically on the local machine 7 | build --flaky_test_attempts=1 8 | 9 | build:buildbuddy --remote_header=x-buildbuddy-api-key=YOUR_API_KEY 10 | -------------------------------------------------------------------------------- /util.bzl: -------------------------------------------------------------------------------- 1 | def _common_root_as_var(ctx): 2 | if ctx.attr.var_name != "": 3 | key = ctx.attr.var_name 4 | else: 5 | key = ctx.label.name.upper() 6 | 7 | value = ctx.files.srcs[0].dirname 8 | for src in ctx.files.srcs: 9 | if value.startswith(src.dirname): 10 | value = src.dirname 11 | elif src.dirname.startswith(value): 12 | pass 13 | elif value == src.dirname: 14 | pass 15 | else: 16 | fail("%s and %s do not share a common root" % (value, src.dirname)) 17 | 18 | return [ 19 | platform_common.TemplateVariableInfo({ 20 | key: value, 21 | }), 22 | ] 23 | 24 | common_root_as_var = rule( 25 | implementation = _common_root_as_var, 26 | attrs = { 27 | "var_name": attr.string(), 28 | "srcs": attr.label_list( 29 | allow_files = True, 30 | mandatory = True, 31 | ), 32 | }, 33 | ) 34 | --------------------------------------------------------------------------------