Skip to content

Complex code sections

Andrej Božić edited this page Jun 3, 2024 · 4 revisions

Introduction

This page documents a complex code sections within the project. Here, you'll find a detailed breakdown to enhance understanding. Expect to find line-by-line explanations, comments, and potentially diagrams or visualizations for clarity.

Layout's and routing in mobile app

Root Layout Component

The Layout component serves as the root layout for the application. It sets up the necessary providers and themes for the entire app. Here's a breakdown:

  • It imports necessary dependencies like QueryClientProvider, CurrentUserContextProvider, and AuthProvider to manage state and data fetching.
  • It defines a custom theme using react-native-paper with primary and secondary colors overridden.
  • Inside the QueryClientProvider, it wraps the CurrentUserContextProvider and AuthProvider, ensuring that the entire app has access to user authentication and current user context.
  • The Slot component is rendered within the PaperProvider, which acts as a placeholder for rendering different screens/components based on navigation.

Layout for Authentication Check

The Layout component responsible for checking user authentication status. Here's what it does:

  • It manages the loading state while verifying the user's authentication status.
  • Utilizes hooks like useRefreshToken and useAuth to manage authentication-related functionality.
  • If the user is not authenticated (no access token), it redirects to the login screen.
  • If users token is not active(user just registered his account) it renders preferences page, otherwise it renders main app

Layout for Protected Routes

This component manages the layout for protected routes, ensuring that only authenticated users can access certain screens. Here are the details:

  • It uses Tabs from expo-router to create a tab-based navigation interface.
  • Each tab represents a different screen, with corresponding icons and options.

Layout for Setup Preferences

This component handles the layout for setting up user preferences, divided into multiple steps. Here's how it works:

  • It uses createStackNavigator from @react-navigation/stack to create a stack-based navigation flow.
  • Each step of preference setup is represented by a separate screen component.

Cyclomatic complexity for Match Service

Match service contains the most important code for out application. It is important for us that code is both simple and fast. So with this analasys we will go through all methods that are contained in match service. Methods in match service are as follows:

  • findMatch
  • finishMatch
  • findPotentialMatches
  • filterUsersByPreferences
  • getAllSuccessfulMatchedUsers
  • findMatchedUserById

Cyclomatic complexity findMatch

public Match findMatch(String userId1, String userId2) {
    Optional<Match> match1 = matchRepository.findByFirstUserIdAndSecondUserId(userId1, userId2);

    if (match1.isPresent()) {
        return match1.get();
    }

    Optional<Match> match2 = matchRepository.findByFirstUserIdAndSecondUserId(userId2, userId1);

    return match2.orElse(null);
}

Control flow for findMatch:

1 decision point (if (match1.isPresent())) Cyclomatic complexity: 𝑀=𝐸−𝑁+2𝑃 𝐸=4,𝑁=3,𝑃=1 𝑀=4−3+2 𝑀=3

Cyclomatic complexity finishMatch

public Match finishMatch(Match match, boolean status) {
    match.setFinished(true);
    match.setAccepted(status);
    return matchRepository.save(match);
}

Control flow for finishMatch:

No decision points Cyclomatic complexity: M=E−N+2P E=3,N=3,P=1 M=3−3+2 M=2

Cyclomatic complexity findPotentialMatches

public List<DisplayUserDto> findPotentialMatches(User user, int skip) {
    List<User> mappedResults = mongoTemplate.aggregate(
            Aggregation.newAggregation(
                    Aggregation.sort(Direction.DESC, "_id"),
                    Aggregation.match(
                            Criteria.where("active").is(true)
                                    .and("_id").nin(user.getSeenUserIds())
                                    .and("age").lte(user.getPreferences().getAgeGroupMax())
                                    .and("gender").is(user.getPreferences().getPartnerGender())
                                    .and("location").is(user.getLocation())
                                    .and("email").ne(user.getEmail())),
                    Aggregation.match(Criteria.where("age").gte(user.getPreferences().getAgeGroupMin())),
                    Aggregation.skip(skip),
                    Aggregation.limit(5)),
            "users", User.class).getMappedResults();

    List<User> filteredList = new ArrayList<>();

    filterUsersByPreferences(user, mappedResults, filteredList);

    return DisplayUserConverter.convertToDtoList(filteredList);
}

Control flow for findPotentialMatches:

No decision points directly within this method Cyclomatic complexity: M=E−N+2P E=3,N=3,P=1 M=3−3+2 M=2

Cyclomatic complexity getAllSuccessfulMatchedUsers

public List<DisplayUserDto> getAllSuccessfulMatchedUsers(User user) {
    MatchOperation matchOperation = Aggregation.match(
            Criteria.where("finished").is(true)
                    .and("accepted").is(true)
                    .andOperator(
                            new Criteria().orOperator(
                                    Criteria.where("firstUserId").is(user.getId().toString()),
                                    Criteria.where("secondUserId").is(user.getId().toString()))));

    ProjectionOperation projectionOperation = Aggregation.project().and(
                    ConditionalOperators
                            .when(ComparisonOperators.Eq.valueOf("firstUserId").equalToValue(user.getId().toString()))
                            .thenValueOf("secondUserId")
                            .otherwiseValueOf("firstUserId"))
            .as("matchedUserId");

    Aggregation aggregation = Aggregation.newAggregation(matchOperation, projectionOperation);

    List<MatchedUserIdDTO> matchedIds = mongoTemplate.aggregate(aggregation, "matches", MatchedUserIdDTO.class).getMappedResults();

    List<String> matchedUserIds = matchedIds.stream()
            .map(matchedUserDTO -> matchedUserDTO.getMatchedUserId())
            .collect(Collectors.toList());

    List<DisplayUserDto> userDtos = DisplayUserConverter.convertToDtoList(userRepository.findAllById(matchedUserIds));

    for (DisplayUserDto userDto : userDtos) {
        userDto.setChatId(findMatch(user.getId(), userDto.getId()).getId());
    }

    Map<String, Message> lastMessages = new HashMap<>();
    for (DisplayUserDto userDto : userDtos) {
        lastMessages.put(userDto.getId(), messageRepository.findFirstByChatIdOrderByTimestampDesc(userDto.getChatId()));
    }

    userDtos.sort(Comparator.comparing((DisplayUserDto userDto) -> {
        Message lastMessage = lastMessages.get(userDto.getId());
        return (lastMessage != null) ? lastMessage.getTimestamp() : null;
    }, Comparator.nullsLast(Comparator.reverseOrder())));

    return userDtos;
}

Control flow for getAllSuccessfulMatchedUsers:

1 decision point (if (lastMessage != null) inside the sort comparator) Cyclomatic complexity: M=E−N+2P E=4,N=3,P=1 M=4−3+2 M=3

Cyclomatic complexity filterUsersByPreferences

public static void filterUsersByPreferences(User currentUser, List<User> userList, List<User> filteredList) {
    for (User user : userList) {
        Preferences listPreferences = user.getPreferences();

        if (listPreferences != null) {
            // Check age group
            if (currentUser.getPreferences() != null &&
                    (currentUser.getAge() < listPreferences.getAgeGroupMin() ||
                            currentUser.getAge() > listPreferences.getAgeGroupMax())) {
                continue; // Skip this user
            }

            // Check gender preference
            if (currentUser.getPreferences() != null &&
                    !currentUser.getGender().equals(listPreferences.getPartnerGender())) {
                continue; // Skip this user
            }

            // Add the user to the filtered list
            filteredList.add(user);
        }
    }
}

Control flow for filterUsersByPreferences:

3 decision points Cyclomatic complexity: M=E−N+2P M=10−5+2×1 M=10−5+2 M=7

Profiling Overview

Tools and Methods

Profiling was conducted using the built-in IntelliJ profiler in IntelliJ IDEA. The requests were simulated through a Postman run.

Profiling Results

In the profiling output, the yellow-highlighted methods represent our application-specific methods.

Key Findings

  1. JwtAuthenticationFilter.doFilterInternal Method

    • This method appears to consume a significant amount of processing time.
    • Filters in a web application act like middleware, executing on each request.
    • Upon closer inspection, our custom code within this filter takes approximately 250 ms.
    • The majority of the time is spent in the doFilter method of the next filter, which proceeds to call the subsequent filter in the chain.
    • Conclusion: Our implementation in this method is performing adequately.
  2. Fetching Matched Users

    • Methods related to fetching single or multiple matched users show potential for performance improvements.
    • These methods search the matches collection using user IDs stored under the fields firstUserId and secondUserId.
    • Potential Issue: These fields are not indexed, leading to slower search operations.
    • Optimization Suggestion: Index the firstUserId and secondUserId fields to improve search performance.

Detailed Analysis

Below are screenshots from the profiling session:

Overall Profiling Summary

Profiling Summary This summary highlights the methods consuming the most processing time. The JwtAuthenticationFilter.doFilterInternal is a notable entry.

Method Call Details

Method Call Details This detail shows the breakdown of time spent in various methods within the application.

Profiling Insights on Fetching Matches

Fetching Matches The time spent on fetching matched users indicates a potential slowdown due to the lack of indexing on critical fields.

Recommendations

  1. Optimize Filtering Process:

    • Review the JwtAuthenticationFilter.doFilterInternal implementation to ensure it’s as efficient as possible.
    • Consider optimizing or simplifying the logic if feasible, though current performance seems reasonable.
  2. Index Database Fields:

    • Index the firstUserId and secondUserId fields in the matches collection to enhance search performance.
    • This should significantly reduce the time required for fetching matched users.

By addressing these areas, we can improve the overall performance of the application, ensuring faster response times and a smoother user experience.